"""Cross-book terminology consistency report. For every translated verse of a book, finds which glossary terms occur in the source text and whether the translation uses the glossary's Japanese rendering (or a discouraged one). Aggregates per-term so drift is visible at a glance — e.g. λόγος rendered 言葉 in twelve verses but 言 in two. Purely mechanical (no LLM calls). Usage: uv run python scripts/report_terminology.py [ ...] """ from __future__ import annotations import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from src.context.glossary import GlossaryManager # noqa: E402 from src.data.loader import DataLoader # noqa: E402 from src.models.verse import VerseReference # noqa: E402 _VERB_ENDINGS = "るすうくぐつぬぶむ" def _rendering_present(japanese: str, text: str) -> bool: """Check a glossary rendering against text, tolerating verb conjugation. 義と認める won't literally appear in 義と認められる and 罪を犯す won't appear in 罪を犯し — for renderings ending in a dictionary-form verb ending, also accept the stem (rendering minus the final kana). """ if japanese in text: return True if len(japanese) > 2 and japanese[-1] in _VERB_ENDINGS and japanese[:-1] in text: return True return False def _accepted_renderings(term) -> list[str]: """All acceptable renderings: the primary plus semantic_range alternates.""" return [term.japanese] + [ alt for alt in getattr(term, "semantic_range", []) if alt ] def report_book(book: str, data_dir: Path) -> int: """Print the terminology report for one book. Returns issue count.""" loader = DataLoader(data_dir) glossary = GlossaryManager(loader.load_glossary()) output_dir = data_dir / "output" / book if not output_dir.exists(): print(f"[{book}] no translations found") return 0 # term.original -> {"used": [refs], "missing": [refs], "avoided": [(ref, bad)]} stats: dict[str, dict] = {} for chapter_file in sorted(output_dir.glob("chapter_*.json")): if chapter_file.stem.endswith("_eval"): continue data = json.loads(chapter_file.read_text(encoding="utf-8")) chapter = data["chapter"] for verse_num, vdata in data["verses"].items(): text = vdata.get("text", "") if not text: continue ref = VerseReference(book=book, chapter=chapter, verse=int(verse_num)) source_verse = loader.load_verse(ref) source = source_verse.greek_text or source_verse.hebrew_text if not source: continue for term in glossary.find_terms_in_text(source): entry = stats.setdefault( term.original, {"used": [], "missing": [], "avoided": []} ) label = f"{chapter}:{verse_num}" for bad in term.avoid_translations: if bad and bad in text: entry["avoided"].append((label, bad)) if any( _rendering_present(r, text) for r in _accepted_renderings(term) ): entry["used"].append(label) else: entry["missing"].append(label) issues = 0 print(f"\n=== Terminology report: {book} ===") for original, entry in sorted(stats.items()): used, missing, avoided = entry["used"], entry["missing"], entry["avoided"] term = glossary.get_by_original(original) flag = "" if avoided: flag = " << DISCOURAGED RENDERING USED" issues += len(avoided) elif used and missing: flag = " << INCONSISTENT" issues += len(missing) elif missing and not used: # Never rendered with the glossary term — may be inflection or # legitimate paraphrase; informational only. flag = " (glossary rendering not found — check manually)" print( f" {original} → {term.japanese if term else '?'}: " f"used {len(used)}, without {len(missing)}{flag}" ) for label in entry["missing"][:5]: if used: # only detail actual inconsistencies print(f" without glossary rendering: {label}") for label, bad in avoided[:5]: print(f" uses discouraged '{bad}': {label}") if not stats: print(" (no glossary terms matched)") return issues def main() -> int: books = sys.argv[1:] if not books: print(__doc__) return 2 data_dir = Path(__file__).parent.parent / "data" total = 0 for book in books: total += report_book(book, data_dir) print(f"\nTotal potential issues: {total}") return 0 if __name__ == "__main__": main()