feat(release): parse_release returns (ParsedRelease, ParseReport)

Wire the scoring foundations into the parser entry point. parse_release
now returns a tuple — the structural ParsedRelease and a diagnostic
ParseReport carrying confidence (0-100), road
(EASY / SHITTY / PATH_OF_PAIN), the residual UNKNOWN tokens, and the
list of critical fields that couldn't be filled.

EASY is decided structurally (a group schema matched), independently
of the score. SHITTY vs PATH_OF_PAIN is decided by score against the
60 cutoff from scoring.yaml. Malformed names (forbidden chars) emit a
zero-confidence PoP report and short-circuit to parse_path=AI as
before.

ParsePath stays as-is (DIRECT / SANITIZED / AI) — it records *how* we
tokenized, not how confident we are. The two dimensions are now
properly separated.

Call sites propagated:
- alfred/application/filesystem/resolve_destination.py (4 occurrences)
- alfred/agent/tools/filesystem.py
- tests/domain/test_release.py
- tests/domain/test_release_fixtures.py
- tests/application/test_detect_media_type.py

New tests/domain/release/test_parser_v2_scoring.py (22 cases) locks
ParseReport validation, compute_score arithmetic, decide_road
thresholding, the collector helpers, and the end-to-end tuple contract.
This commit is contained in:
2026-05-20 01:21:30 +02:00
parent 98c688f29b
commit b4c9efd13b
7 changed files with 336 additions and 22 deletions
+3 -2
View File
@@ -42,9 +42,10 @@ def test_parse_matches_fixture(fixture: ReleaseFixture, tmp_path) -> None:
# plausible filesystem paths. Catches typos / missing leading dirs early.
fixture.materialize(tmp_path)
result = asdict(parse_release(fixture.release_name, _KB))
parsed, _report = parse_release(fixture.release_name, _KB)
result = asdict(parsed)
# ``is_season_pack`` is a @property — asdict() does not include it.
result["is_season_pack"] = parse_release(fixture.release_name, _KB).is_season_pack
result["is_season_pack"] = parsed.is_season_pack
for field, expected in fixture.expected_parsed.items():
assert field in result, (