sorter: deepest-matching-dir re-homing + weak 0.50 confidence tier
Inner directory names now compete with the pack/file name: a subtree under a dir whose name matches the catalog better than the pack-level resolution is re-homed there (fixes multi-unit sculptor packs like 'Retro Elvez' dumping everything into one keyword class). Junk dir names (files/images/HEADS/2/...) never match. Hits between 0.50 and the threshold are accepted as low-confidence: [w] in the plan, confidence in the audit, listed in sorter_low_confidence.txt. Works in both provenance and manual-tree mode.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""Offline sorter tests: dir re-homing, weak tier, junk dirs (plain python3)."""
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from dlphelper.sorter import ( # noqa: E402
|
||||
apply_plan,
|
||||
load_catalog,
|
||||
sort_plan,
|
||||
)
|
||||
|
||||
CATALOG = {
|
||||
"entries": [
|
||||
{"name": "Warp Spiders", "system": "Warhammer 40,000",
|
||||
"group": "Xenos Armies", "faction": "Aeldari",
|
||||
"class": "Fly", "aliases": []},
|
||||
{"name": "Harlequin Troupe", "system": "Warhammer 40,000",
|
||||
"group": "Xenos Armies", "faction": "Harlequins",
|
||||
"class": "Battleline", "aliases": []},
|
||||
{"name": "Guardian Squad", "system": "Warhammer 40,000",
|
||||
"group": "Xenos Armies", "faction": "Aeldari",
|
||||
"class": "Battleline", "aliases": []},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def make_lib():
|
||||
return Path(tempfile.mkdtemp(prefix="sorter_test_lib_"))
|
||||
|
||||
|
||||
def write(p: Path):
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_bytes(b"x")
|
||||
|
||||
|
||||
_LOADED = None
|
||||
|
||||
|
||||
def get_catalog():
|
||||
global _LOADED
|
||||
if _LOADED is None:
|
||||
_LOADED = load_catalog_path()
|
||||
return _LOADED
|
||||
|
||||
|
||||
def prov_items(root: Path, library):
|
||||
(root / "extract_manifest.jsonl").write_text(json.dumps({
|
||||
"archive": str(root / "Multi Pack.zip"),
|
||||
"dest": "Multi Pack", "unit": "Multi Pack.zip",
|
||||
}), encoding="utf-8")
|
||||
files = sorted((root / "Multi Pack").rglob("*"))
|
||||
return sort_plan(files, library, get_catalog(), provenance={
|
||||
"Multi Pack": "Multi Pack.zip"}, root=root)
|
||||
|
||||
|
||||
def test_prov_dir_rehome():
|
||||
"""A matching inner dir name claims its subtree; junk dirs don't."""
|
||||
root = Path(tempfile.mkdtemp(prefix="sorter_test_src_"))
|
||||
pack = root / "Multi Pack"
|
||||
write(pack / "14 Warp spiders" / "spider_body.stl")
|
||||
write(pack / "14 Warp spiders" / "files" / "spider_arm.stl")
|
||||
write(pack / "HEADS" / "head_a.stl") # junk dir: stays with pack
|
||||
write(pack / "Retro GrimDark Thing" / "t.stl") # no catalog match
|
||||
library = make_lib()
|
||||
items = prov_items(root, library)
|
||||
tgt = {i.name: i.target for i in items}
|
||||
lib = str(library)
|
||||
# warp spiders re-homed, incl. nested junk dir structure, class from catalog
|
||||
assert "Warp Spiders" in str(tgt["spider_body.stl"]), tgt["spider_body.stl"]
|
||||
assert "Fly" in str(tgt["spider_body.stl"]), tgt["spider_body.stl"]
|
||||
assert "Hero" not in str(tgt["spider_body.stl"])
|
||||
assert "files" in str(tgt["spider_arm.stl"])
|
||||
# junk-dir file stays under the pack unit folder
|
||||
assert "_unsorted/Multi Pack/HEADS" in str(tgt["head_a.stl"]).replace("\\", "/")
|
||||
# unmatched subtree keeps the pack unit
|
||||
assert "Multi Pack" in str(tgt["t.stl"])
|
||||
shutil.rmtree(root); shutil.rmtree(library)
|
||||
|
||||
|
||||
def test_prov_weak_tier():
|
||||
"""Below-threshold dir hit is accepted as low confidence."""
|
||||
root = Path(tempfile.mkdtemp(prefix="sorter_test_src_"))
|
||||
pack = root / "Multi Pack"
|
||||
# 'guardian squad extras' scores below 1.0 but high vs 'Guardian Squad'
|
||||
write(pack / "guardian squad extras" / "g.stl")
|
||||
library = make_lib()
|
||||
items = prov_items(root, library)
|
||||
it = next(i for i in items if i.name == "g.stl")
|
||||
assert it.match_kind in ("weak", "catalog"), it.match_kind
|
||||
if it.match_kind == "weak":
|
||||
assert it.confidence == "low"
|
||||
assert "Guardian" in str(it.target)
|
||||
shutil.rmtree(root); shutil.rmtree(library)
|
||||
|
||||
|
||||
def test_nonprov_rehome():
|
||||
"""Loose noise file inside a named dir re-homes to the dir's unit."""
|
||||
root = Path(tempfile.mkdtemp(prefix="sorter_test_src_"))
|
||||
write(root / "Warp Spiders" / "body.stl")
|
||||
library = make_lib()
|
||||
files = sorted(root.rglob("*"))
|
||||
items = sort_plan(files, library, get_catalog(), root=root)
|
||||
it = next(i for i in items if i.name == "body.stl")
|
||||
assert it.match_kind == "catalog", (it.match_kind, it.target)
|
||||
assert "Warp Spiders" in str(it.target) and "Fly" in str(it.target)
|
||||
shutil.rmtree(root); shutil.rmtree(library)
|
||||
|
||||
|
||||
def test_apply_weak_doc():
|
||||
"""apply_plan writes the low-confidence review document."""
|
||||
root = Path(tempfile.mkdtemp(prefix="sorter_test_src_"))
|
||||
pack = root / "Multi Pack"
|
||||
write(pack / "guardian squad extras" / "g.stl")
|
||||
library = make_lib()
|
||||
items = prov_items(root, library)
|
||||
linked, copied, skipped = apply_plan(items, library / "sorter_applied.jsonl")
|
||||
doc = library / "sorter_low_confidence.txt"
|
||||
kinds = [i.match_kind for i in items if i.match_kind == "weak"]
|
||||
if kinds:
|
||||
assert doc.exists(), "weak doc missing"
|
||||
assert "low-confidence" in doc.read_text(encoding="utf-8")
|
||||
else:
|
||||
assert not doc.exists()
|
||||
assert (library / "sorter_applied.jsonl").exists()
|
||||
shutil.rmtree(root); shutil.rmtree(library)
|
||||
|
||||
|
||||
def load_catalog_path():
|
||||
import tempfile as tf
|
||||
fh = tf.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
|
||||
json.dump(CATALOG, fh)
|
||||
fh.close()
|
||||
return load_catalog(Path(fh.name))
|
||||
|
||||
|
||||
def main():
|
||||
catalog = load_catalog_path()
|
||||
for fn in (test_prov_dir_rehome, test_prov_weak_tier, test_nonprov_rehome,
|
||||
test_apply_weak_doc):
|
||||
fn()
|
||||
print("OK", fn.__name__)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user