Catalogue: classify collection into system/faction/force-org class

- dlphelper/warhammer_data.py: keyword taxonomy (40k factions incl. Chaos and
  Xenos super-faction units, plus AoS/Heresy/Kill Team/Necromunda/Epic/OW)
- dlphelper/catalogue.py: manifest+directory scanner, conservative keyword
  classifier (plural-tolerant word boundaries, longest match wins), markdown
  and jsonl reports, overrides support, audited --apply moves
- catalogue.py CLI; tests/test_catalogue.py offline classifier test
- real-data check on the user's dirs: 1624/3333 files classified so far
This commit is contained in:
2026-10-04 20:25:21 +02:00
parent 64abd7126f
commit acb3415878
6 changed files with 848 additions and 7 deletions
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Offline classifier test for the catalogue (no network, no deps).
Run with: python3 tests/test_catalogue.py
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from dlphelper.catalogue import classify # noqa: E402
CASES = [
# (filename, expected system, expected faction, expected class or None-any)
("Thunderwolf Cavalry.7z", "Warhammer 40,000", "Adeptus Astartes", "Fast Attack"),
("Wulfen.zip", "Warhammer 40,000", "Adeptus Astartes", "Elites"),
("Keeper of Secrets & Shalaxi Helbane.7z", "Warhammer 40,000", "Chaos Daemons", "HQ"),
("Assorted Drones.zip", "Warhammer 40,000", "T'au Empire", None),
("Commander Shadowsun.zip", "Warhammer 40,000", "T'au Empire", "HQ"),
("Devilfish.zip", "Warhammer 40,000", "T'au Empire", "Dedicated Transport"),
("Battle Sister Scanz.zip", "Warhammer 40,000", "Adepta Sororitas", None),
("Knights.zip", None, None, None), # ambiguous keyword alone: unclassified
("Khorne Demon Enginez and skull lordz.zip", "Warhammer 40,000", "Chaos Daemons", None),
("Cultists.zip", "Warhammer 40,000", "Chaos Space Marines", "Elites"),
("Da Troopz.zip", "Warhammer 40,000", "Orks", None),
("Seraphon.zip", "Other Systems", "Age of Sigmar", None),
("Leviathan Dreadnought.zip", "Warhammer 40,000", "Adeptus Astartes", "Elites"),
("Poxwalkers.zip", "Warhammer 40,000", "Death Guard", "Elites"),
("Kabalite Warriors.zip", "Warhammer 40,000", "Drukhari", "Troops"),
("Canoptek Wraiths.zip", "Warhammer 40,000", "Necrons", "Fast Attack"),
("random thing with no keywords.zip", None, None, None),
]
def main() -> None:
failures = 0
for name, exp_system, exp_faction, exp_class in CASES:
system, faction, klass = classify(name)
problems = []
if exp_system and system != exp_system:
problems.append(f"system={system!r} != {exp_system!r}")
if exp_faction and faction != exp_faction:
problems.append(f"faction={faction!r} != {exp_faction!r}")
if exp_class and klass != exp_class:
problems.append(f"class={klass!r} != {exp_class!r}")
if problems:
failures += 1
print(f"FAIL {name!r}: {', '.join(problems)}")
else:
print(f"ok {name!r} -> {system} / {faction} / {klass}")
if failures:
print(f"\n{failures} failures")
sys.exit(1)
print("\ntest_catalogue: OK")
if __name__ == "__main__":
main()