173 lines
5.8 KiB
Python
173 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify ADR-0084 definitional closure across mounted definitional packs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
DATA_ROOT = REPO_ROOT / "packs" / "data"
|
|
PRIMITIVES_PATH = REPO_ROOT / "packs" / "primitives" / "en_semantic_primitives_v1" / "primitives.jsonl"
|
|
TOKEN_RE = re.compile(r"[A-Za-z_]+")
|
|
STOPWORDS = {"is", "of", "to", "by", "the", "a", "an", "that", "which"}
|
|
PREPOSITIONS = {
|
|
"in",
|
|
"on",
|
|
"at",
|
|
"for",
|
|
"with",
|
|
"from",
|
|
"into",
|
|
"between",
|
|
"before",
|
|
"after",
|
|
"during",
|
|
"while",
|
|
"until",
|
|
"since",
|
|
"through",
|
|
"within",
|
|
"without",
|
|
"as",
|
|
"under",
|
|
"over",
|
|
"about",
|
|
"against",
|
|
"toward",
|
|
"towards",
|
|
"across",
|
|
"than",
|
|
}
|
|
SKIP_TOKENS = STOPWORDS | PREPOSITIONS
|
|
|
|
|
|
def _load_jsonl(path: Path) -> list[dict]:
|
|
rows: list[dict] = []
|
|
if not path.exists():
|
|
return rows
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
if line.strip():
|
|
rows.append(json.loads(line))
|
|
return rows
|
|
|
|
|
|
def _load_resolution_set() -> set[str]:
|
|
lemmas: set[str] = set()
|
|
for lex_path in DATA_ROOT.glob("*/lexicon.jsonl"):
|
|
for row in _load_jsonl(lex_path):
|
|
for field_name in ("lemma", "surface"):
|
|
value = row.get(field_name)
|
|
if isinstance(value, str):
|
|
lemmas.add(value)
|
|
for gloss_path in DATA_ROOT.glob("*/glosses.jsonl"):
|
|
for row in _load_jsonl(gloss_path):
|
|
lemma = row.get("lemma")
|
|
if isinstance(lemma, str):
|
|
lemmas.add(lemma)
|
|
for row in _load_jsonl(PRIMITIVES_PATH):
|
|
lemma = row.get("lemma")
|
|
if isinstance(lemma, str):
|
|
lemmas.add(lemma)
|
|
return lemmas
|
|
|
|
|
|
def _definitional_pack_manifests() -> list[Path]:
|
|
manifests: list[Path] = []
|
|
for manifest_path in DATA_ROOT.glob("*/manifest.json"):
|
|
try:
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
continue
|
|
if manifest.get("definitional_layer") is True:
|
|
manifests.append(manifest_path)
|
|
return sorted(manifests)
|
|
|
|
|
|
def _content_tokens(gloss: str) -> list[str]:
|
|
tokens = [token.lower() for token in TOKEN_RE.findall(gloss)]
|
|
ordered: list[str] = []
|
|
for token in tokens:
|
|
if token in SKIP_TOKENS:
|
|
continue
|
|
if token not in ordered:
|
|
ordered.append(token)
|
|
return ordered
|
|
|
|
|
|
def verify() -> dict:
|
|
resolution_set = _load_resolution_set()
|
|
total_entries = 0
|
|
total_atoms = 0
|
|
unresolved_by_pack: dict[str, list[str]] = defaultdict(list)
|
|
mismatched_atoms_by_pack: dict[str, list[str]] = defaultdict(list)
|
|
|
|
for manifest_path in _definitional_pack_manifests():
|
|
pack_dir = manifest_path.parent
|
|
gloss_path = pack_dir / "glosses.jsonl"
|
|
pack_id = pack_dir.name
|
|
for row in _load_jsonl(gloss_path):
|
|
total_entries += 1
|
|
atoms = row.get("definitional_atoms", [])
|
|
if not isinstance(atoms, list):
|
|
unresolved_by_pack[pack_id].append(f"{row.get('lemma','<unknown>')}:<non-list-atoms>")
|
|
continue
|
|
gloss = row.get("gloss")
|
|
if not isinstance(gloss, str):
|
|
mismatched_atoms_by_pack[pack_id].append(f"{row.get('lemma','<unknown>')}:<missing-gloss>")
|
|
continue
|
|
expected_atoms = _content_tokens(gloss)
|
|
if atoms != expected_atoms:
|
|
mismatched_atoms_by_pack[pack_id].append(
|
|
f"{row.get('lemma','<unknown>')}:expected={expected_atoms}:actual={atoms}"
|
|
)
|
|
for atom in atoms:
|
|
total_atoms += 1
|
|
if not isinstance(atom, str) or atom not in resolution_set:
|
|
unresolved_by_pack[pack_id].append(f"{row.get('lemma','<unknown>')}:{atom}")
|
|
|
|
unresolved_by_pack = {pack: items for pack, items in unresolved_by_pack.items() if items}
|
|
mismatched_atoms_by_pack = {pack: items for pack, items in mismatched_atoms_by_pack.items() if items}
|
|
return {
|
|
"total_entries_checked": total_entries,
|
|
"total_definitional_atoms_checked": total_atoms,
|
|
"unresolved_by_pack": unresolved_by_pack,
|
|
"mismatched_atoms_by_pack": mismatched_atoms_by_pack,
|
|
"definitional_pack_count": len(_definitional_pack_manifests()),
|
|
"primitive_pack_path": str(PRIMITIVES_PATH.relative_to(REPO_ROOT)),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
args = parser.parse_args()
|
|
|
|
report = verify()
|
|
if args.json:
|
|
print(json.dumps(report, indent=2, sort_keys=True))
|
|
else:
|
|
print(f"total entries checked: {report['total_entries_checked']}")
|
|
print(f"total definitional_atoms checked: {report['total_definitional_atoms_checked']}")
|
|
if report["unresolved_by_pack"]:
|
|
print("unresolved tokens grouped by pack:")
|
|
for pack, items in sorted(report["unresolved_by_pack"].items()):
|
|
print(f"- {pack}: {', '.join(items)}")
|
|
else:
|
|
print("unresolved tokens grouped by pack: none")
|
|
if report["mismatched_atoms_by_pack"]:
|
|
print("gloss/atom mismatches grouped by pack:")
|
|
for pack, items in sorted(report["mismatched_atoms_by_pack"].items()):
|
|
print(f"- {pack}: {', '.join(items)}")
|
|
else:
|
|
print("gloss/atom mismatches grouped by pack: none")
|
|
return 0 if not report["unresolved_by_pack"] and not report["mismatched_atoms_by_pack"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|