core/language_packs/schema.py
Shay 24daebf3c1 feat(pack-resolver): gloss resolver with lexicon-residency + dual-checksum hardening
Lands the gloss-loader scaffolding from feat/pack-glosses-wip onto
main, with every hardening item from the 2026-05-19 design review
built in from the start.  No glosses ship in this commit — only the
infrastructure that will consume them safely.

Hardening items (each pinned by a test):

1. Lexicon-residency check in resolve_gloss()
   chat/pack_resolver.py — resolve_gloss now requires the lemma to be
   present in the same pack's lexicon.jsonl BEFORE consulting
   glosses.jsonl.  Without this, glosses.jsonl would become a parallel
   surface-authoring channel that bypasses the lexicon's checksum
   seal: someone could ship a gloss for a lemma the pack never
   ratified, and the runtime would emit it as if it were pack content.

   Test: TestLexiconResidencyEnforced::test_gloss_for_unratified_lemma_is_rejected
   authors a gloss for ``gamma`` (a lemma not in the lexicon) and
   asserts resolve_gloss returns None.

2. Dual-checksum manifest support
   language_packs/schema.py — LanguagePackManifest gains an OPTIONAL
   ``glosses_checksum: str | None`` field.  Glosses are an additive
   overlay; bumping the glosses_checksum does NOT perturb the
   immutable lexicon checksum.
   language_packs/compiler.py — _load_pack_cached now verifies
   bytes-on-disk of glosses.jsonl against the manifest's
   glosses_checksum when present.  Missing field on legacy packs is
   back-compat (no verification, no raise).  Mismatch raises
   ValueError exactly like the lexicon checksum gate.

   Tests:
     test_matching_glosses_checksum_loads_clean — happy path
     test_checksum_mismatch_raises — tampered file rejected
     test_missing_glosses_checksum_is_back_compat — legacy packs OK

3. clear_resolver_cache() clears BOTH lexicon AND glosses LRU caches
   Previously only cleared _pack_lexicon_for, so test fixtures that
   wrote glosses.jsonl mid-process would see stale (empty) gloss data
   on subsequent resolve_gloss calls.

   Test: TestClearResolverCacheClearsBoth proves the issue exists
   without the clear, then proves the new code fixes it.

4. Malformed JSONL lines silently skipped
   A single bad line in glosses.jsonl must not break resolution for
   the rest of the pack.  Same defensive parsing as _pack_lexicon_for.
   Entries missing required fields (lemma, gloss, or empty values)
   are also skipped.

   Tests:
     test_malformed_line_skipped — invalid JSON between valid lines
     test_entry_missing_required_field_skipped — 4 bad shapes filtered

5. Missing glosses.jsonl is back-compat
   _pack_glosses_for returns an empty dict when the file is absent.
   resolve_gloss returns None.  No exception.  All 9 currently-
   ratified English packs ship with no glosses.jsonl — they must
   continue to load cleanly.

   Tests:
     test_pack_with_no_glosses_returns_empty
     test_resolve_gloss_on_lemma_without_gloss_file_returns_none

Files:
  chat/pack_resolver.py
    + _pack_glosses_for (cached loader)
    + resolve_gloss (lexicon-residency-gated lookup)
    * clear_resolver_cache now clears both caches
  language_packs/schema.py
    + LanguagePackManifest.glosses_checksum field (optional)
  language_packs/compiler.py
    + dual-checksum verification block in _load_pack_cached
    + glosses_checksum field passed through to the manifest dataclass
  tests/test_pack_resolver_glosses.py
    11 tests covering all five hardening items

Verification:
  11/11 new tests green.
  Full cognition eval byte-identical.
  All currently-ratified packs continue to load without glosses.
2026-05-19 07:24:36 -07:00

190 lines
6.6 KiB
Python

"""
Schemas for CORE compiled linguistic manifolds.
A language pack is not a dataset. It is a deterministic, checksummed,
compiled linguistic manifold: lexical surfaces, morphology, grammar
attractors, cross-language resonances, and holonomy-level proof cases.
These schemas intentionally do not load corpora. They define the contract the
Supervised Seeding Epoch must satisfy before Hebrew and Koine Greek gates can
engage.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Mapping, Sequence
class LanguageRole(str, Enum):
"""Architectural role of a language pack in CORE-Logos."""
OPERATIONAL_BASE = "operational_base"
ARTICULATION_SURFACE = "articulation_surface"
DEPTH_ROOT = "depth_root"
DEPTH_RELATION = "depth_relation"
class OOVPolicy(str, Enum):
"""Out-of-vocabulary behavior for a pack."""
FAIL_CLOSED = "fail_closed"
TAGGED_FALLBACK = "tagged_fallback"
PROPOSE_VOCAB_EXPANSION = "propose_vocab_expansion"
@dataclass(frozen=True, slots=True)
class LanguagePackManifest:
"""Pinned manifest for one compiled language pack."""
pack_id: str
language: str
role: LanguageRole
script: str
normalization_policy: str
source_manifest: str
determinism_class: str
checksum: str
version: str = "1.0.0"
gate_engaged: bool = False
oov_policy: OOVPolicy = OOVPolicy.FAIL_CLOSED
# Optional dual-checksum for the companion ``glosses.jsonl`` file.
# When present, the loader verifies the bytes-on-disk match this
# SHA-256 just like the lexicon checksum. Absent on legacy packs
# that ship no glosses (back-compat — never raised in that case).
# Glosses are an additive overlay; bumping ``glosses_checksum`` does
# NOT perturb the immutable ``checksum`` (lexicon seal).
glosses_checksum: str | None = None
def __post_init__(self) -> None:
if not self.pack_id:
raise ValueError("LanguagePackManifest.pack_id is required.")
if not self.language:
raise ValueError("LanguagePackManifest.language is required.")
if not self.checksum:
raise ValueError("LanguagePackManifest.checksum is required.")
if self.role in {LanguageRole.DEPTH_ROOT, LanguageRole.DEPTH_RELATION}:
if self.gate_engaged and self.oov_policy is not OOVPolicy.FAIL_CLOSED:
raise ValueError(
"Depth packs must fail closed while gate_engaged=True; "
"unknown Hebrew/Greek surfaces must not collapse to a fallback point."
)
@dataclass(frozen=True, slots=True)
class MorphologyEntry:
"""
Morphological decomposition for a surface form.
Ordering is load-bearing. For Semitic root morphology and Koine grammar,
non-commutative composition means prefix/stem/inflection/suffix order must
be preserved exactly.
"""
morphology_id: str
surface: str
lemma: str
language: str
root: str | None = None
prefix_chain: tuple[str, ...] = field(default_factory=tuple)
stem: str | None = None
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:
"""One surface/lemma entry in a compiled linguistic manifold.
`epistemic_status` follows ADR-0021: it is a *position in the
revision graph*, not a source-trust tier. The default is
``"speculative"`` per ADR-0021 §Schema impact: "transitions to
COHERENT / CONTESTED / FALSIFIED only via the review path." A pack
lexicon row that wants to be admissible as evidence
(``ADMISSIBLE_AS_EVIDENCE``) must declare
``"epistemic_status": "coherent"`` explicitly; the declaration is
itself the curator's stamp. Pack authority alone is not coherence
judgment — defaulting unmarked rows to COHERENT would re-import the
bias ADR-0021 refuses (see ``docs/truth_seeking_schema.md`` §1).
"""
entry_id: str
surface: str
lemma: str
language: str
part_of_speech: str | None = None
pos: str | None = None
morphology_id: str | None = None
morphology_tags: tuple[str, ...] = field(default_factory=tuple)
semantic_domains: tuple[str, ...] = field(default_factory=tuple)
manifold_point_checksum: str | None = None
provenance_ids: tuple[str, ...] = field(default_factory=tuple)
epistemic_status: str = "speculative"
@dataclass(frozen=True, slots=True)
class GrammarAttractor:
"""
Structural grammar attractor seeded into the shared manifold.
Morphology is operator composition. Semantic domain is attractor geometry.
Alignment is resonance. This class represents the attractor layer only.
"""
attractor_id: str
language: str
role: str
description: str
operator_order: tuple[str, ...] = field(default_factory=tuple)
checksum: str | None = None
@dataclass(frozen=True, slots=True)
class AlignmentEdge:
"""Weighted directional resonance between entries or concepts."""
source_id: str
target_id: str
relation: str
weight: float
evidence_ids: tuple[str, ...] = field(default_factory=tuple)
def __post_init__(self) -> None:
if not 0.0 <= self.weight <= 1.0:
raise ValueError("AlignmentEdge.weight must be in [0, 1].")
@dataclass(frozen=True, slots=True)
class HolonomyAlignmentCase:
"""
Crown proof case for the three-language design.
The language system succeeds when aligned canonical clauses produce nearby
holonomies without flattening their distinctions. This is not token-level
translation; it is dynamic field-path resonance.
"""
case_id: str
description: str
source_refs: tuple[str, ...]
pack_ids: tuple[str, ...]
expected_relation: str
negative_source_refs: tuple[str, ...] = field(default_factory=tuple)
tolerance: float | None = None
def __post_init__(self) -> None:
if len(self.source_refs) < 2:
raise ValueError("HolonomyAlignmentCase requires at least two source_refs.")
if len(self.pack_ids) < 2:
raise ValueError("HolonomyAlignmentCase requires at least two pack_ids.")