diff --git a/language_packs/compiler.py b/language_packs/compiler.py index cfbc5517..8f898d40 100644 --- a/language_packs/compiler.py +++ b/language_packs/compiler.py @@ -142,4 +142,18 @@ def load_pack_entries(pack_id: str) -> list[LexicalEntry]: for line in lexicon_path.read_text(encoding="utf-8").splitlines(): if line.strip(): entries.append(_parse_entry(json.loads(line))) + _validate_morphology_links(pack_id, entries) return entries + + +def _validate_morphology_links(pack_id: str, entries: list[LexicalEntry]) -> None: + morphology_ids = [entry.morphology_id for entry in entries if entry.morphology_id] + if not morphology_ids: + return + + from morphology.registry import load_morphology + + registry = load_morphology(pack_id) + missing = [morphology_id for morphology_id in morphology_ids if registry.get(morphology_id) is None] + if missing: + raise ValueError(f"{pack_id}: dangling morphology_id link(s): {', '.join(missing)}") diff --git a/language_packs/data/grc_logos_micro_v1/lexicon.jsonl b/language_packs/data/grc_logos_micro_v1/lexicon.jsonl index 29d10529..dc5e2894 100644 --- a/language_packs/data/grc_logos_micro_v1/lexicon.jsonl +++ b/language_packs/data/grc_logos_micro_v1/lexicon.jsonl @@ -1,7 +1,7 @@ -{"entry_id": "grc-001", "surface": "\u03bb\u03cc\u03b3\u03bf\u03c2", "lemma": "\u03bb\u03cc\u03b3\u03bf\u03c2", "language": "grc", "pos": "NOUN", "semantic_domains": ["logos.utterance.word", "logos.core", "communication.articulation", "divine.revelation", "existence.utterance"], "morphology_tags": ["noun", "masculine", "nominative", "singular", "second_declension", "root:\u03bb\u03b5\u03b3"]} -{"entry_id": "grc-002", "surface": "\u1f00\u03c1\u03c7\u03ae", "lemma": "\u1f00\u03c1\u03c7\u03ae", "language": "grc", "pos": "NOUN", "semantic_domains": ["logos.genesis.origin", "logos.core", "origin", "existence.ground", "temporal.absolute"], "morphology_tags": ["noun", "feminine", "nominative", "singular", "first_declension", "root:\u1f00\u03c1\u03c7"]} -{"entry_id": "grc-003", "surface": "\u03c6\u1ff6\u03c2", "lemma": "\u03c6\u1ff6\u03c2", "language": "grc", "pos": "NOUN", "semantic_domains": ["logos.illumination.photon", "logos.core", "logos.illumination", "divine.revelation", "knowledge.clarity"], "morphology_tags": ["noun", "neuter", "nominative", "singular", "third_declension", "root:\u03c6\u03c9\u03c4"]} -{"entry_id": "grc-004", "surface": "\u03b6\u03c9\u03ae", "lemma": "\u03b6\u03c9\u03ae", "language": "grc", "pos": "NOUN", "semantic_domains": ["logos.vitality.animate", "logos.core", "vitality", "existence.manifestation", "being.animate"], "morphology_tags": ["noun", "feminine", "nominative", "singular", "first_declension", "root:\u03b6\u03b1"]} -{"entry_id": "grc-005", "surface": "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1", "lemma": "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1", "language": "grc", "pos": "NOUN", "semantic_domains": ["logos.aletheia.verity", "logos.core", "epistemic.ground", "knowledge.ground", "divine.revelation"], "morphology_tags": ["noun", "feminine", "nominative", "singular", "first_declension", "alpha_privative", "root:\u03bb\u03b1\u03b8"]} -{"entry_id": "grc-006", "surface": "\u03c0\u03bd\u03b5\u1fe6\u03bc\u03b1", "lemma": "\u03c0\u03bd\u03b5\u1fe6\u03bc\u03b1", "language": "grc", "pos": "NOUN", "semantic_domains": ["logos.pneuma.breath", "logos.core", "breath", "existence.animating", "logos.presence"], "morphology_tags": ["noun", "neuter", "nominative", "singular", "third_declension", "root:\u03c0\u03bd\u03b5\u03c5"]} -{"entry_id": "grc-007", "surface": "\u03ba\u03c4\u03af\u03b6\u03c9", "lemma": "\u03ba\u03c4\u03af\u03b6\u03c9", "language": "grc", "pos": "VERB", "semantic_domains": ["logos.ktizo.formation", "logos.core", "formation", "logos.genesis", "divine.agency"], "morphology_tags": ["verb", "present", "active", "indicative", "first_person", "singular", "root:\u03ba\u03c4\u03b9\u03b4"]} +{"entry_id": "grc-001", "surface": "\u03bb\u03cc\u03b3\u03bf\u03c2", "lemma": "\u03bb\u03cc\u03b3\u03bf\u03c2", "language": "grc", "pos": "NOUN", "morphology_id": "grc-morph-001", "semantic_domains": ["logos.utterance.word", "logos.core", "communication.articulation", "divine.revelation", "existence.utterance"], "morphology_tags": ["noun", "masculine", "nominative", "singular", "second_declension", "root:\u03bb\u03b5\u03b3"]} +{"entry_id": "grc-002", "surface": "\u1f00\u03c1\u03c7\u03ae", "lemma": "\u1f00\u03c1\u03c7\u03ae", "language": "grc", "pos": "NOUN", "morphology_id": "grc-morph-002", "semantic_domains": ["logos.genesis.origin", "logos.core", "origin", "existence.ground", "temporal.absolute"], "morphology_tags": ["noun", "feminine", "nominative", "singular", "first_declension", "root:\u1f00\u03c1\u03c7"]} +{"entry_id": "grc-003", "surface": "\u03c6\u1ff6\u03c2", "lemma": "\u03c6\u1ff6\u03c2", "language": "grc", "pos": "NOUN", "morphology_id": "grc-morph-003", "semantic_domains": ["logos.illumination.photon", "logos.core", "logos.illumination", "divine.revelation", "knowledge.clarity"], "morphology_tags": ["noun", "neuter", "nominative", "singular", "third_declension", "root:\u03c6\u03c9\u03c4"]} +{"entry_id": "grc-004", "surface": "\u03b6\u03c9\u03ae", "lemma": "\u03b6\u03c9\u03ae", "language": "grc", "pos": "NOUN", "morphology_id": "grc-morph-004", "semantic_domains": ["vitality.animate", "vitality", "existence.manifestation", "being.animate"], "morphology_tags": ["noun", "feminine", "nominative", "singular", "first_declension", "root:\u03b6\u03b1"]} +{"entry_id": "grc-005", "surface": "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1", "lemma": "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1", "language": "grc", "pos": "NOUN", "morphology_id": "grc-morph-005", "semantic_domains": ["logos.aletheia.verity", "logos.core", "epistemic.ground", "knowledge.ground", "divine.revelation"], "morphology_tags": ["noun", "feminine", "nominative", "singular", "first_declension", "alpha_privative", "root:\u03bb\u03b1\u03b8"]} +{"entry_id": "grc-006", "surface": "\u03c0\u03bd\u03b5\u1fe6\u03bc\u03b1", "lemma": "\u03c0\u03bd\u03b5\u1fe6\u03bc\u03b1", "language": "grc", "pos": "NOUN", "morphology_id": "grc-morph-006", "semantic_domains": ["logos.pneuma.breath", "logos.core", "breath", "existence.animating", "logos.presence"], "morphology_tags": ["noun", "neuter", "nominative", "singular", "third_declension", "root:\u03c0\u03bd\u03b5\u03c5"]} +{"entry_id": "grc-007", "surface": "\u03ba\u03c4\u03af\u03b6\u03c9", "lemma": "\u03ba\u03c4\u03af\u03b6\u03c9", "language": "grc", "pos": "VERB", "morphology_id": "grc-morph-007", "semantic_domains": ["logos.ktizo.formation", "logos.core", "formation", "logos.genesis", "divine.agency"], "morphology_tags": ["verb", "present", "active", "indicative", "first_person", "singular", "root:\u03ba\u03c4\u03b9\u03b4"]} diff --git a/language_packs/data/grc_logos_micro_v1/manifest.json b/language_packs/data/grc_logos_micro_v1/manifest.json index 9723a4af..2e6373ac 100644 --- a/language_packs/data/grc_logos_micro_v1/manifest.json +++ b/language_packs/data/grc_logos_micro_v1/manifest.json @@ -6,7 +6,7 @@ "normalization_policy": "unitize_versor", "source_manifest": "grc_logos_micro_v1.lexicon.jsonl", "determinism_class": "D0", - "checksum": "1fea9d9c775d49a1dd17857c240afcd60e22c625e10601aa90e22b10d9e70262", + "checksum": "462d0aee88714f04e003d0a3f9d9015f460ba39657c4b8dff8dfd2ed24d75a3f", "version": "1.0.0", "gate_engaged": true, "oov_policy": "fail_closed" diff --git a/language_packs/data/grc_logos_micro_v1/morphology.jsonl b/language_packs/data/grc_logos_micro_v1/morphology.jsonl new file mode 100644 index 00000000..d0dcefd4 --- /dev/null +++ b/language_packs/data/grc_logos_micro_v1/morphology.jsonl @@ -0,0 +1,7 @@ +{"morphology_id": "grc-morph-001", "surface": "\u03bb\u03cc\u03b3\u03bf\u03c2", "lemma": "\u03bb\u03cc\u03b3\u03bf\u03c2", "language": "grc", "root": "\u03bb\u03b5\u03b3", "prefix_chain": [], "stem": "\u03bb\u03bf\u03b3", "inflection": {"pos": "noun", "case": "nominative", "gender": "masculine", "number": "singular", "declension": "second"}, "suffix_chain": ["\u03bf\u03c2"]} +{"morphology_id": "grc-morph-002", "surface": "\u1f00\u03c1\u03c7\u03ae", "lemma": "\u1f00\u03c1\u03c7\u03ae", "language": "grc", "root": "\u1f00\u03c1\u03c7", "prefix_chain": [], "stem": "\u1f00\u03c1\u03c7", "inflection": {"pos": "noun", "case": "nominative", "gender": "feminine", "number": "singular", "declension": "first"}, "suffix_chain": ["\u03ae"]} +{"morphology_id": "grc-morph-003", "surface": "\u03c6\u1ff6\u03c2", "lemma": "\u03c6\u1ff6\u03c2", "language": "grc", "root": "\u03c6\u03c9\u03c4", "prefix_chain": [], "stem": "\u03c6\u03c9\u03c4", "inflection": {"pos": "noun", "case": "nominative", "gender": "neuter", "number": "singular", "declension": "third"}, "suffix_chain": ["\u03c2"]} +{"morphology_id": "grc-morph-004", "surface": "\u03b6\u03c9\u03ae", "lemma": "\u03b6\u03c9\u03ae", "language": "grc", "root": "\u03b6\u03b1", "prefix_chain": [], "stem": "\u03b6\u03c9", "inflection": {"pos": "noun", "case": "nominative", "gender": "feminine", "number": "singular", "declension": "first"}, "suffix_chain": ["\u03ae"]} +{"morphology_id": "grc-morph-005", "surface": "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1", "lemma": "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1", "language": "grc", "root": "\u03bb\u03b1\u03b8", "prefix_chain": ["\u1f00"], "stem": "\u03bb\u03b7\u03b8\u03b5\u03b9", "inflection": {"pos": "noun", "case": "nominative", "gender": "feminine", "number": "singular", "declension": "first"}, "suffix_chain": ["\u03b1"]} +{"morphology_id": "grc-morph-006", "surface": "\u03c0\u03bd\u03b5\u1fe6\u03bc\u03b1", "lemma": "\u03c0\u03bd\u03b5\u1fe6\u03bc\u03b1", "language": "grc", "root": "\u03c0\u03bd\u03b5\u03c5", "prefix_chain": [], "stem": "\u03c0\u03bd\u03b5\u03c5\u03bc\u03b1\u03c4", "inflection": {"pos": "noun", "case": "nominative", "gender": "neuter", "number": "singular", "declension": "third"}, "suffix_chain": []} +{"morphology_id": "grc-morph-007", "surface": "\u03ba\u03c4\u03af\u03b6\u03c9", "lemma": "\u03ba\u03c4\u03af\u03b6\u03c9", "language": "grc", "root": "\u03ba\u03c4\u03b9\u03b4", "prefix_chain": [], "stem": "\u03ba\u03c4\u03b9\u03b6", "inflection": {"pos": "verb", "tense": "present", "voice": "active", "mood": "indicative", "person": "first", "number": "singular"}, "suffix_chain": ["\u03c9"]} diff --git a/language_packs/data/he_logos_micro_v1/lexicon.jsonl b/language_packs/data/he_logos_micro_v1/lexicon.jsonl index 2e94e2c3..3b052c9c 100644 --- a/language_packs/data/he_logos_micro_v1/lexicon.jsonl +++ b/language_packs/data/he_logos_micro_v1/lexicon.jsonl @@ -1,7 +1,7 @@ -{"entry_id": "he-001", "surface": "\u05d3\u05d1\u05e8", "lemma": "\u05d3\u05d1\u05e8", "language": "he", "pos": "NOUN", "semantic_domains": ["logos.utterance.word", "logos.core", "communication.articulation", "divine.revelation", "existence.utterance"], "morphology_tags": ["noun", "masculine", "singular", "root:\u05d3\u05d1\u05e8", "triliteral:D-B-R"]} -{"entry_id": "he-002", "surface": "\u05e8\u05d0\u05e9\u05d9\u05ea", "lemma": "\u05e8\u05d0\u05e9\u05d9\u05ea", "language": "he", "pos": "NOUN", "semantic_domains": ["logos.genesis.origin", "logos.core", "origin", "existence.ground", "temporal.absolute"], "morphology_tags": ["noun", "feminine", "singular", "root:\u05e8\u05d0\u05e9", "construct_state"]} -{"entry_id": "he-003", "surface": "\u05d0\u05d5\u05e8", "lemma": "\u05d0\u05d5\u05e8", "language": "he", "pos": "NOUN", "semantic_domains": ["logos.illumination.photon", "logos.core", "logos.illumination", "divine.revelation", "knowledge.clarity"], "morphology_tags": ["noun", "masculine", "singular", "root:\u05d0\u05d5\u05e8", "triliteral:A-W-R"]} -{"entry_id": "he-004", "surface": "\u05d7\u05d9\u05d9\u05dd", "lemma": "\u05d7\u05d9\u05d9\u05dd", "language": "he", "pos": "NOUN", "semantic_domains": ["logos.vitality.animate", "logos.core", "vitality", "existence.manifestation", "being.animate"], "morphology_tags": ["noun", "masculine", "plural", "root:\u05d7\u05d9\u05d4", "triliteral:H-Y-H"]} -{"entry_id": "he-005", "surface": "\u05d0\u05de\u05ea", "lemma": "\u05d0\u05de\u05ea", "language": "he", "pos": "NOUN", "semantic_domains": ["logos.aletheia.verity", "logos.core", "epistemic.ground", "knowledge.ground", "divine.revelation"], "morphology_tags": ["noun", "feminine", "singular", "root:\u05d0\u05de\u05df", "triliteral:A-M-N"]} -{"entry_id": "he-006", "surface": "\u05e8\u05d5\u05d7", "lemma": "\u05e8\u05d5\u05d7", "language": "he", "pos": "NOUN", "semantic_domains": ["logos.pneuma.breath", "logos.core", "breath", "existence.animating", "logos.presence"], "morphology_tags": ["noun", "feminine", "singular", "root:\u05e8\u05d5\u05d7", "triliteral:R-W-H"]} -{"entry_id": "he-007", "surface": "\u05d1\u05e8\u05d0", "lemma": "\u05d1\u05e8\u05d0", "language": "he", "pos": "VERB", "semantic_domains": ["logos.ktizo.formation", "logos.core", "formation", "logos.genesis", "divine.agency"], "morphology_tags": ["verb", "qal", "perfect", "third_person", "masculine", "singular", "root:\u05d1\u05e8\u05d0", "triliteral:B-R-A"]} +{"entry_id": "he-001", "surface": "\u05d3\u05d1\u05e8", "lemma": "\u05d3\u05d1\u05e8", "language": "he", "pos": "NOUN", "morphology_id": "he-morph-001", "semantic_domains": ["logos.utterance.word", "logos.core", "communication.articulation", "divine.revelation", "existence.utterance"], "morphology_tags": ["noun", "masculine", "singular", "root:\u05d3\u05d1\u05e8", "triliteral:D-B-R"]} +{"entry_id": "he-002", "surface": "\u05e8\u05d0\u05e9\u05d9\u05ea", "lemma": "\u05e8\u05d0\u05e9\u05d9\u05ea", "language": "he", "pos": "NOUN", "morphology_id": "he-morph-002", "semantic_domains": ["logos.genesis.origin", "logos.core", "origin", "existence.ground", "temporal.absolute"], "morphology_tags": ["noun", "feminine", "singular", "root:\u05e8\u05d0\u05e9", "construct_state"]} +{"entry_id": "he-003", "surface": "\u05d0\u05d5\u05e8", "lemma": "\u05d0\u05d5\u05e8", "language": "he", "pos": "NOUN", "morphology_id": "he-morph-003", "semantic_domains": ["logos.illumination.photon", "logos.core", "logos.illumination", "divine.revelation", "knowledge.clarity"], "morphology_tags": ["noun", "masculine", "singular", "root:\u05d0\u05d5\u05e8", "triliteral:A-W-R"]} +{"entry_id": "he-004", "surface": "\u05d7\u05d9\u05d9\u05dd", "lemma": "\u05d7\u05d9\u05d9\u05dd", "language": "he", "pos": "NOUN", "morphology_id": "he-morph-004", "semantic_domains": ["logos.vitality.animate", "logos.core", "vitality", "existence.manifestation", "being.animate"], "morphology_tags": ["noun", "masculine", "plural", "root:\u05d7\u05d9\u05d4", "triliteral:H-Y-H"]} +{"entry_id": "he-005", "surface": "\u05d0\u05de\u05ea", "lemma": "\u05d0\u05de\u05ea", "language": "he", "pos": "NOUN", "morphology_id": "he-morph-005", "semantic_domains": ["logos.aletheia.verity", "logos.core", "epistemic.ground", "knowledge.ground", "divine.revelation"], "morphology_tags": ["noun", "feminine", "singular", "root:\u05d0\u05de\u05df", "triliteral:A-M-N"]} +{"entry_id": "he-006", "surface": "\u05e8\u05d5\u05d7", "lemma": "\u05e8\u05d5\u05d7", "language": "he", "pos": "NOUN", "morphology_id": "he-morph-006", "semantic_domains": ["logos.pneuma.breath", "logos.core", "breath", "existence.animating", "logos.presence"], "morphology_tags": ["noun", "feminine", "singular", "root:\u05e8\u05d5\u05d7", "triliteral:R-W-H"]} +{"entry_id": "he-007", "surface": "\u05d1\u05e8\u05d0", "lemma": "\u05d1\u05e8\u05d0", "language": "he", "pos": "VERB", "morphology_id": "he-morph-007", "semantic_domains": ["logos.ktizo.formation", "logos.core", "formation", "logos.genesis", "divine.agency"], "morphology_tags": ["verb", "qal", "perfect", "third_person", "masculine", "singular", "root:\u05d1\u05e8\u05d0", "triliteral:B-R-A"]} diff --git a/language_packs/data/he_logos_micro_v1/manifest.json b/language_packs/data/he_logos_micro_v1/manifest.json index 5e86d934..7a4546f7 100644 --- a/language_packs/data/he_logos_micro_v1/manifest.json +++ b/language_packs/data/he_logos_micro_v1/manifest.json @@ -6,7 +6,7 @@ "normalization_policy": "unitize_versor", "source_manifest": "he_logos_micro_v1.lexicon.jsonl", "determinism_class": "D0", - "checksum": "ea1ac85dbdbfe01bebe97356279ba76bdf6653e46a19f31e3e5c8050e33b601d", + "checksum": "6e2f30175937c56cae276007d9865555beb5dd0759925660ab259c54a67d7b1d", "version": "1.0.0", "gate_engaged": true, "oov_policy": "fail_closed" diff --git a/language_packs/data/he_logos_micro_v1/morphology.jsonl b/language_packs/data/he_logos_micro_v1/morphology.jsonl new file mode 100644 index 00000000..213277bc --- /dev/null +++ b/language_packs/data/he_logos_micro_v1/morphology.jsonl @@ -0,0 +1,7 @@ +{"morphology_id": "he-morph-001", "surface": "\u05d3\u05d1\u05e8", "lemma": "\u05d3\u05d1\u05e8", "language": "he", "root": "\u05d3-\u05d1-\u05e8", "prefix_chain": [], "stem": "\u05d3\u05d1\u05e8", "inflection": {"pos": "noun", "gender": "masculine", "number": "singular"}, "suffix_chain": []} +{"morphology_id": "he-morph-002", "surface": "\u05e8\u05d0\u05e9\u05d9\u05ea", "lemma": "\u05e8\u05d0\u05e9\u05d9\u05ea", "language": "he", "root": "\u05e8-\u05d0-\u05e9", "prefix_chain": [], "stem": "\u05e8\u05d0\u05e9", "inflection": {"pos": "noun", "gender": "feminine", "number": "singular", "state": "construct"}, "suffix_chain": ["\u05d9\u05ea"]} +{"morphology_id": "he-morph-003", "surface": "\u05d0\u05d5\u05e8", "lemma": "\u05d0\u05d5\u05e8", "language": "he", "root": "\u05d0-\u05d5-\u05e8", "prefix_chain": [], "stem": "\u05d0\u05d5\u05e8", "inflection": {"pos": "noun", "gender": "masculine", "number": "singular"}, "suffix_chain": []} +{"morphology_id": "he-morph-004", "surface": "\u05d7\u05d9\u05d9\u05dd", "lemma": "\u05d7\u05d9\u05d9\u05dd", "language": "he", "root": "\u05d7-\u05d9-\u05d4", "prefix_chain": [], "stem": "\u05d7\u05d9", "inflection": {"pos": "noun", "gender": "masculine", "number": "plural"}, "suffix_chain": ["\u05d9\u05dd"]} +{"morphology_id": "he-morph-005", "surface": "\u05d0\u05de\u05ea", "lemma": "\u05d0\u05de\u05ea", "language": "he", "root": "\u05d0-\u05de-\u05df", "prefix_chain": [], "stem": "\u05d0\u05de\u05ea", "inflection": {"pos": "noun", "gender": "feminine", "number": "singular"}, "suffix_chain": []} +{"morphology_id": "he-morph-006", "surface": "\u05e8\u05d5\u05d7", "lemma": "\u05e8\u05d5\u05d7", "language": "he", "root": "\u05e8-\u05d5-\u05d7", "prefix_chain": [], "stem": "\u05e8\u05d5\u05d7", "inflection": {"pos": "noun", "gender": "feminine", "number": "singular"}, "suffix_chain": []} +{"morphology_id": "he-morph-007", "surface": "\u05d1\u05e8\u05d0", "lemma": "\u05d1\u05e8\u05d0", "language": "he", "root": "\u05d1-\u05e8-\u05d0", "prefix_chain": [], "stem": "\u05d1\u05e8\u05d0", "inflection": {"pos": "verb", "binyan": "qal", "aspect": "perfect", "person": "third", "gender": "masculine", "number": "singular"}, "suffix_chain": []} diff --git a/language_packs/schema.py b/language_packs/schema.py index 4075b7d9..ee8766c3 100644 --- a/language_packs/schema.py +++ b/language_packs/schema.py @@ -75,6 +75,7 @@ class MorphologyEntry: be preserved exactly. """ + morphology_id: str surface: str lemma: str language: str @@ -84,6 +85,16 @@ class MorphologyEntry: inflection: Mapping[str, str] = field(default_factory=dict) suffix_chain: tuple[str, ...] = field(default_factory=tuple) + def __post_init__(self) -> None: + if not self.morphology_id: + raise ValueError("MorphologyEntry.morphology_id is required.") + if not self.surface: + raise ValueError("MorphologyEntry.surface is required.") + if not self.lemma: + raise ValueError("MorphologyEntry.lemma is required.") + if not self.language: + raise ValueError("MorphologyEntry.language is required.") + @dataclass(frozen=True, slots=True) class LexicalEntry: diff --git a/morphology/__init__.py b/morphology/__init__.py new file mode 100644 index 00000000..b53bcd11 --- /dev/null +++ b/morphology/__init__.py @@ -0,0 +1,5 @@ +"""Morphology registry for language-pack root and inflection structure.""" + +from .registry import MorphologyRegistry, load_morphology + +__all__ = ["MorphologyRegistry", "load_morphology"] diff --git a/morphology/registry.py b/morphology/registry.py new file mode 100644 index 00000000..508eb18a --- /dev/null +++ b/morphology/registry.py @@ -0,0 +1,86 @@ +""" +Morphology registry — reconstructive root and inflection structure. + +The registry is deliberately pure schema + stdlib. It does not compile +versors, import algebra, or participate in propagation. Its job is to make +LexicalEntry.morphology_id resolve to ordered morphological structure. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from language_packs.schema import MorphologyEntry + +_DATA_DIR = Path(__file__).parent.parent / "language_packs" / "data" + + +class MorphologyRegistry: + """Immutable morphology lookup table for one language pack.""" + + def __init__(self, entries: list[MorphologyEntry]) -> None: + self._entries: tuple[MorphologyEntry, ...] = tuple(entries) + self._by_id: dict[str, MorphologyEntry] = {} + self._by_surface: dict[str, MorphologyEntry] = {} + for entry in self._entries: + if entry.morphology_id in self._by_id: + raise ValueError(f"Duplicate morphology_id: {entry.morphology_id}") + self._by_id[entry.morphology_id] = entry + self._by_surface[entry.surface] = entry + + def __len__(self) -> int: + return len(self._entries) + + @property + def entries(self) -> tuple[MorphologyEntry, ...]: + return self._entries + + def get(self, morphology_id: str) -> MorphologyEntry | None: + """Return an entry by morphology_id, or None if absent.""" + return self._by_id.get(morphology_id) + + def for_surface(self, surface: str) -> MorphologyEntry | None: + """Return an entry by surface form, or None if absent.""" + return self._by_surface.get(surface) + + def require(self, morphology_id: str) -> MorphologyEntry: + """Return an entry by morphology_id, raising if the link is broken.""" + entry = self.get(morphology_id) + if entry is None: + raise KeyError(f"Morphology id '{morphology_id}' not in registry.") + return entry + + +def _parse_entry(payload: dict) -> MorphologyEntry: + return MorphologyEntry( + morphology_id=payload["morphology_id"], + surface=payload["surface"], + lemma=payload["lemma"], + language=payload["language"], + root=payload.get("root"), + prefix_chain=tuple(payload.get("prefix_chain", [])), + stem=payload.get("stem"), + inflection=dict(payload.get("inflection", {})), + suffix_chain=tuple(payload.get("suffix_chain", [])), + ) + + +def load_morphology(pack_id: str) -> MorphologyRegistry: + """ + Load MorphologyEntry records from language_packs/data//morphology.jsonl. + + Packs without morphology data return an empty registry; packs that set + LexicalEntry.morphology_id are validated by the compiler against this + registry. + """ + morphology_path = _DATA_DIR / pack_id / "morphology.jsonl" + if not morphology_path.exists(): + return MorphologyRegistry([]) + + entries: list[MorphologyEntry] = [] + for line in morphology_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if line: + entries.append(_parse_entry(json.loads(line))) + return MorphologyRegistry(entries) diff --git a/tests/test_morphology_registry.py b/tests/test_morphology_registry.py new file mode 100644 index 00000000..06409889 --- /dev/null +++ b/tests/test_morphology_registry.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import pytest + +from language_packs import load_pack_entries +from morphology.registry import MorphologyRegistry, load_morphology + + +def test_hebrew_morphology_registry_loads_triliteral_roots(): + registry = load_morphology("he_logos_micro_v1") + + assert len(registry) == 7 + davar = registry.require("he-morph-001") + assert davar.surface == "\u05d3\u05d1\u05e8" + assert davar.root == "\u05d3-\u05d1-\u05e8" + assert davar.inflection["gender"] == "masculine" + + +def test_greek_morphology_registry_preserves_ordered_prefix_and_suffix(): + registry = load_morphology("grc_logos_micro_v1") + + aletheia = registry.for_surface("\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1") + assert aletheia is not None + assert aletheia.prefix_chain == ("\u1f00",) + assert aletheia.stem == "\u03bb\u03b7\u03b8\u03b5\u03b9" + assert aletheia.suffix_chain == ("\u03b1",) + assert aletheia.inflection["case"] == "nominative" + + +def test_depth_pack_lexicon_entries_resolve_morphology_ids(): + registry = load_morphology("he_logos_micro_v1") + entries = load_pack_entries("he_logos_micro_v1") + + assert all(entry.morphology_id for entry in entries) + for entry in entries: + morph = registry.require(entry.morphology_id or "") + assert morph.surface == entry.surface + assert morph.lemma == entry.lemma + + +def test_pack_without_morphology_returns_empty_registry(): + registry = load_morphology("en_minimal_v1") + assert len(registry) == 0 + assert registry.get("missing") is None + + +def test_morphology_registry_rejects_duplicate_ids(): + entry = load_morphology("he_logos_micro_v1").require("he-morph-001") + with pytest.raises(ValueError, match="Duplicate morphology_id"): + MorphologyRegistry([entry, entry])