Add morphology registry for language packs

This commit is contained in:
Shay 2026-05-13 14:50:36 -07:00
parent a4b4d22987
commit 8d09c2a8c1
11 changed files with 196 additions and 16 deletions

View file

@ -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)}")

View file

@ -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"]}

View file

@ -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"

View file

@ -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"]}

View file

@ -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"]}

View file

@ -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"

View file

@ -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": []}

View file

@ -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:

5
morphology/__init__.py Normal file
View file

@ -0,0 +1,5 @@
"""Morphology registry for language-pack root and inflection structure."""
from .registry import MorphologyRegistry, load_morphology
__all__ = ["MorphologyRegistry", "load_morphology"]

86
morphology/registry.py Normal file
View file

@ -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/<pack_id>/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)

View file

@ -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])