sorter: outer file name wins over inner archive names in fuzzy match

Best-pool scoring used to always blend the archive's inner file names
into the fuzzy score (0.95 discount), which dragged clean-named files
into wrong units (e.g. 'Keeper of Secrets & Shalaxi Helbane.7z' ended
in Orks/Boyz). Now the outer name decides when it scores >= threshold;
inner names only vote as fallback (hash-named gofile archives rely on
that). Also: url/failed-list run artifacts are excluded from sorting,
and two plan-inspection helpers under tools/.
This commit is contained in:
2026-10-05 07:34:29 +02:00
parent ecf01df13b
commit da5f1f7315
3 changed files with 66 additions and 5 deletions
+18 -5
View File
@@ -35,6 +35,7 @@ RUN_ARTIFACTS = {
"export.json", "export.html", "state.json", "manifest.jsonl",
"failed_urls.txt", "catalogue.md", "catalogue.jsonl",
"sorter_applied.jsonl", "catalogue_moves.jsonl", "urls.txt",
"urls_0807.txt", "urls_gofile.txt", "failed_0807.txt",
}
@@ -174,13 +175,19 @@ def _best_ratio(clean: str, candidates: list[str]) -> float:
)
def _best_in_pool(item: SortItem, pool: list[dict]) -> tuple[float, dict] | None:
"""Best fuzzy score of `item` against all entries in `pool`."""
def _best_in_pool(
item: SortItem, pool: list[dict], use_inner: bool = True
) -> tuple[float, dict] | None:
"""Best fuzzy score of `item` against all entries in `pool`.
Inner archive names are only consulted on request; a confident outer
(file) name must win over votes from random inner STL filenames.
"""
best: tuple[float, dict] | None = None
for entry in pool:
cands = [entry["_clean"], *entry["_clean_aliases"]]
score = _best_ratio(item.clean, cands)
if item.inner_names:
if use_inner and item.inner_names:
inner_clean = [_clean_name(n) for n in item.inner_names[:10]]
for c in inner_clean:
score = max(score, _best_ratio(c, cands) * 0.95) # inner slightly discounted
@@ -202,13 +209,19 @@ def match_catalog(item: SortItem, catalog: list[dict], threshold: float) -> None
return # HTML-page masquerading as an archive; never fuzzy-match it
same_faction = [e for e in catalog if e.get("faction") == item.faction]
pool = same_faction or catalog # fall back to whole catalog if faction unknown
best = _best_in_pool(item, pool)
best = _best_in_pool(item, pool, use_inner=False)
if best is None or best[0] < threshold:
# Outer name not confident: let the archive's inner file names vote
# (this is the only signal for hash-named gofile archives).
best = _best_in_pool(item, pool, use_inner=True)
if best is None or best[0] < threshold:
# The keyword tables can lag behind GW's current categorization
# (e.g. units moved between factions); if the faction pool has no
# confident match, try the whole catalog before giving up.
if same_faction:
best = _best_in_pool(item, catalog)
best = _best_in_pool(item, catalog, use_inner=False)
if best is None or best[0] < threshold:
best = _best_in_pool(item, catalog, use_inner=True)
if best and best[0] >= threshold:
entry = best[1]
item.unit = entry["name"]