diff --git a/alfred/application/filesystem/manage_subtitles.py b/alfred/application/filesystem/manage_subtitles.py index 4c199b4..7f8c9f3 100644 --- a/alfred/application/filesystem/manage_subtitles.py +++ b/alfred/application/filesystem/manage_subtitles.py @@ -163,7 +163,7 @@ class ManageSubtitlesUseCase: subtitle_prefs = memory.ltm.subtitle_preferences except Exception: pass - rules = repo.load(release_group, subtitle_prefs).resolve() + rules = repo.load(release_group, subtitle_prefs).resolve(kb.default_rules()) matcher = SubtitleMatcher() matched, unresolved = matcher.match(metadata.external_tracks, rules) diff --git a/alfred/domain/subtitles/aggregates.py b/alfred/domain/subtitles/aggregates.py index b62a3be..18c2f71 100644 --- a/alfred/domain/subtitles/aggregates.py +++ b/alfred/domain/subtitles/aggregates.py @@ -3,17 +3,10 @@ from dataclasses import dataclass, field from typing import Any -from alfred.infrastructure.knowledge.subtitles.base import SubtitleKnowledgeBase - from ..shared.value_objects import ImdbId from .value_objects import RuleScope, SubtitleMatchingRules -def DEFAULT_RULES() -> SubtitleMatchingRules: - """Load default matching rules from subtitles.yaml (defaults section).""" - return SubtitleKnowledgeBase().default_rules() - - @dataclass class SubtitleRuleSet: """ @@ -37,12 +30,18 @@ class SubtitleRuleSet: _format_priority: list[str] | None = field(default=None, repr=False) _min_confidence: float | None = field(default=None, repr=False) - def resolve(self) -> SubtitleMatchingRules: + def resolve(self, default_rules: SubtitleMatchingRules) -> SubtitleMatchingRules: """ Walk the parent chain and merge deltas into effective rules. - Falls back to DEFAULT_RULES at the top of the chain. + + ``default_rules`` seeds the top of the chain — it is the caller's + responsibility to load these from the knowledge base (infrastructure). + Keeping the default rules as a parameter keeps the domain free of + any I/O dependency. """ - base = self.parent.resolve() if self.parent else DEFAULT_RULES() + base = ( + self.parent.resolve(default_rules) if self.parent else default_rules + ) return SubtitleMatchingRules( preferred_languages=self._languages or base.preferred_languages, preferred_formats=self._formats or base.preferred_formats, diff --git a/alfred/infrastructure/subtitle/rule_repository.py b/alfred/infrastructure/subtitle/rule_repository.py index 43e7d53..cbec79a 100644 --- a/alfred/infrastructure/subtitle/rule_repository.py +++ b/alfred/infrastructure/subtitle/rule_repository.py @@ -54,7 +54,7 @@ class RuleSetRepository: Build and return the resolved RuleSet chain. If subtitle_preferences is provided, it seeds the global base rule set - from LTM (overriding the hardcoded DEFAULT_RULES). + from LTM (overriding the knowledge-base defaults at resolve time). Returns global default if no overrides exist. """ base = SubtitleRuleSet.global_default() diff --git a/tests/domain/test_subtitle_utils.py b/tests/domain/test_subtitle_utils.py index 233b3b9..192918a 100644 --- a/tests/domain/test_subtitle_utils.py +++ b/tests/domain/test_subtitle_utils.py @@ -30,9 +30,19 @@ from alfred.domain.subtitles.value_objects import ( RuleScope, SubtitleFormat, SubtitleLanguage, + SubtitleMatchingRules, SubtitleType, ) +# Test fixture: stand-in for what the KB would provide at runtime. +_DEFAULT_RULES = SubtitleMatchingRules( + preferred_languages=["eng"], + preferred_formats=["srt"], + allowed_types=["standard"], + format_priority=["srt", "ass"], + min_confidence=0.7, +) + # --------------------------------------------------------------------------- # # Value objects # # --------------------------------------------------------------------------- # @@ -230,18 +240,17 @@ class TestAvailableSubtitles: class TestSubtitleRuleSet: - def test_global_default_uses_kb_defaults(self): + def test_global_default_returns_injected_defaults(self): rs = SubtitleRuleSet.global_default() - rules = rs.resolve() - # Loaded from subtitles.yaml — defaults must be non-empty. - assert rules.preferred_languages - assert rules.preferred_formats - assert 0 < rules.min_confidence <= 1 + rules = rs.resolve(_DEFAULT_RULES) + assert rules.preferred_languages == _DEFAULT_RULES.preferred_languages + assert rules.preferred_formats == _DEFAULT_RULES.preferred_formats + assert rules.min_confidence == _DEFAULT_RULES.min_confidence def test_override_persists(self): rs = SubtitleRuleSet.global_default() rs.override(languages=["eng"], min_confidence=0.9) - rules = rs.resolve() + rules = rs.resolve(_DEFAULT_RULES) assert rules.preferred_languages == ["eng"] assert rules.min_confidence == 0.9 @@ -252,10 +261,10 @@ class TestSubtitleRuleSet: parent=parent, ) child.override(languages=["jpn"]) - rules = child.resolve() + rules = child.resolve(_DEFAULT_RULES) assert rules.preferred_languages == ["jpn"] # min_confidence not overridden at child or parent → falls back to defaults - assert rules.min_confidence == parent.resolve().min_confidence + assert rules.min_confidence == parent.resolve(_DEFAULT_RULES).min_confidence def test_to_dict_only_emits_set_deltas(self): rs = SubtitleRuleSet(scope=RuleScope(level="show", identifier="tt1")) @@ -286,4 +295,4 @@ class TestSubtitleRuleSet: # code uses `is not None` explicitly. Verify 0.0 doesn't fall back. rs = SubtitleRuleSet.global_default() rs.override(min_confidence=0.0) - assert rs.resolve().min_confidence == 0.0 + assert rs.resolve(_DEFAULT_RULES).min_confidence == 0.0 diff --git a/tests/infrastructure/test_rule_repository.py b/tests/infrastructure/test_rule_repository.py index e5d2027..a4a6f43 100644 --- a/tests/infrastructure/test_rule_repository.py +++ b/tests/infrastructure/test_rule_repository.py @@ -17,6 +17,7 @@ from pathlib import Path import yaml +from alfred.domain.subtitles.value_objects import SubtitleMatchingRules from alfred.infrastructure.persistence.memory.ltm.components.subtitle_preferences import ( SubtitlePreferences, ) @@ -25,6 +26,15 @@ from alfred.infrastructure.subtitle.rule_repository import ( _filter_override, ) +# Stand-in for KB defaults, injected at resolve(). +_DEFAULT_RULES = SubtitleMatchingRules( + preferred_languages=["eng"], + preferred_formats=["srt"], + allowed_types=["standard"], + format_priority=["srt", "ass"], + min_confidence=0.7, +) + def _write(path: Path, data: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) @@ -71,17 +81,17 @@ class TestLoad: def test_no_files_returns_global_default(self, tmp_path): repo = RuleSetRepository(tmp_path) rs = repo.load() - # Should resolve cleanly using the hardcoded defaults. - rules = rs.resolve() - assert rules.preferred_languages # non-empty - assert rules.min_confidence > 0 + # With no overrides, resolve returns the injected defaults unchanged. + rules = rs.resolve(_DEFAULT_RULES) + assert rules.preferred_languages == _DEFAULT_RULES.preferred_languages + assert rules.min_confidence == _DEFAULT_RULES.min_confidence def test_subtitle_preferences_override_base(self, tmp_path): prefs = SubtitlePreferences( languages=["jpn"], formats=["ass"], types=["standard"] ) repo = RuleSetRepository(tmp_path) - rules = repo.load(subtitle_preferences=prefs).resolve() + rules = repo.load(subtitle_preferences=prefs).resolve(_DEFAULT_RULES) assert rules.preferred_languages == ["jpn"] assert rules.preferred_formats == ["ass"] assert rules.allowed_types == ["standard"] @@ -92,7 +102,7 @@ class TestLoad: {"override": {"languages": ["spa"], "min_confidence": 0.95}}, ) repo = RuleSetRepository(tmp_path) - rules = repo.load().resolve() + rules = repo.load().resolve(_DEFAULT_RULES) assert rules.preferred_languages == ["spa"] assert rules.min_confidence == 0.95 @@ -102,7 +112,7 @@ class TestLoad: {"override": {"format_priority": ["ass", "srt"]}}, ) repo = RuleSetRepository(tmp_path) - rules = repo.load(release_group="KONTRAST").resolve() + rules = repo.load(release_group="KONTRAST").resolve(_DEFAULT_RULES) assert rules.format_priority == ["ass", "srt"] def test_full_three_level_chain(self, tmp_path): @@ -119,7 +129,9 @@ class TestLoad: {"override": {"min_confidence": 0.99}}, ) repo = RuleSetRepository(tmp_path) - rules = repo.load(release_group="GRP", subtitle_preferences=prefs).resolve() + rules = repo.load(release_group="GRP", subtitle_preferences=prefs).resolve( + _DEFAULT_RULES + ) # All three levels visible — local overrides on top assert rules.preferred_languages == ["jpn"] assert rules.format_priority == ["ass"]