core/tests/test_pack_resolver_glosses.py
Shay 07da601641 feat(packs): seed 323 reviewed glosses across 9 English content packs
Phase C of the gloss feature.  Lands the natural-language gloss
content that the resolver (Phase B2) and the runtime composer
(Phase B3) were prepared for.  This is the user-visible payoff:
cold-start DEFINITION / RECALL prompts on pack-resident lemmas now
emit fluent grounded sentences instead of dotted-domain disclosure.

Authoring: five parallel subagents in ONE message block (a single
parallel dispatch, ~20s wall-clock vs ~95s sequential).  Each
subagent received its pack's complete lemma + POS list and a strict
JSON-shape exemplar.  Total returned: 326 raw gloss entries.

Assembly (this commit): the raw entries were partitioned by
lexicon-residency lookup (the resolve_gloss invariant enforced at
storage time), deduplicated within pack, sorted by lemma, written
to ``language_packs/data/<pack>/glosses.jsonl``, and each pack's
manifest received a new ``glosses_checksum`` field.  323 glosses
landed clean; 0 rejected.

Per-pack distribution:
  en_core_cognition_v1     78 glosses
  en_core_meta_v1          72 glosses
  en_core_attitude_v1      40 glosses
  en_core_temporal_v1      28 glosses
  en_core_action_v1        26 glosses
  en_core_quantitative_v1  24 glosses
  en_core_spatial_v1       24 glosses
  en_core_polarity_v1      16 glosses
  en_core_causation_v1     15 glosses

Live-probe lift (fresh ChatRuntime per prompt):

  BEFORE:
    truth — pack-grounded (en_core_cognition_v1):
      cognition.truth; logos.core; epistemic.ground.
      No session evidence yet.

  AFTER:
    Truth is a claim or state grounded by evidence and coherent
    judgment.  pack-grounded (en_core_cognition_v1).

Same provenance.  Same audit-trail content (the dotted domains are
still in lexicon.jsonl, the resolver can still read them, the
candidate object carries them verbatim).  But the user-facing
surface is a sentence the user can actually read.

Eval-lane lift:

  deterministic_fluency       BEFORE      AFTER
    no_dotted_inventory_rate  0.3333  →   1.0000
    no_provenance_only_rate   1.0000  →   1.0000  (held)
    no_placeholder_rate       1.0000  →   1.0000  (held)
    complete_punctuation_rate 1.0000  →   1.0000  (held)
    finite_predicate_shape    1.0000  →   1.0000  (held)
    surface_provenance_match  1.0000  →   1.0000  (held)
  cold_start_grounding         all metrics held at 1.0
  warmed_session_consistency   no_placeholder + telemetry_match held at 1.0
                              (warm_grounding_stability still 0 — separate fix)
  cognition eval public        100 / 100 / 91.7 / 100   (BYTE-IDENTICAL)
  cognition eval holdout       100 / 100 / 83.3 / 100   (BYTE-IDENTICAL)

  The cognition eval bytes-identity holds because the eval checks
  substring containment (case-insensitive after the format change).
  Every lemma still appears in its fluent surface.

Hardening this commit enforces:

  Lexicon-residency at storage time
    tests/test_pack_glosses_content.py::test_every_gloss_lemma_is_lexicon_resident
    walks every glosses.jsonl and asserts every lemma is present in
    the same pack's lexicon.jsonl.  Drift in glosses (an unratified
    lemma sneaking in) fails the lane immediately.

  Dual-checksum discipline
    tests/test_pack_glosses_content.py::test_every_glossed_pack_has_matching_checksum
    re-hashes glosses.jsonl bytes-on-disk and compares against the
    manifest's glosses_checksum.  Any tampering fails.

  Immutable-lexicon invariant
    tests/test_pack_glosses_content.py::test_lexicon_checksum_unchanged_by_gloss_landing
    re-hashes lexicon.jsonl and compares against the manifest's
    (original) checksum.  Proves that adding glosses did NOT perturb
    the lexicon seal.

  High-freq lemma resolution
    32 of the most-common conversational lemmas (truth, doubt,
    fact, idea, self, true, important, now, place, make, effect,
    always, ...) all resolve to a fluent surface end-to-end.

Test-suite drift this commit absorbed:

  - tests/test_pack_grounding.py — three substring assertions
    updated to be case-insensitive (gloss-backed surfaces capitalize
    lemmas at sentence start, dotted-disclosure surfaces don't).
    "No session evidence yet" assertion replaced with the
    common-substring "pack-grounded" marker that BOTH forms emit.
  - tests/test_pack_resolver_glosses.py — the back-compat test
    pivots from en_core_cognition_v1 (now glossed) to en_minimal_v1
    (deliberately unglossed).  A new test pins the glossed case.

Files added:
  language_packs/data/<pack>/glosses.jsonl  (9 files, 323 entries)
  tests/test_pack_glosses_content.py        (9 contract tests)

Files modified:
  language_packs/data/<pack>/manifest.json  (9 files, glosses_checksum field)
  chat/pack_grounding.py                    (lowercase "pack-grounded" tag)
  tests/test_pack_grounding.py              (3 substring assertions relaxed)
  tests/test_pack_resolver_glosses.py       (back-compat test pivoted)

Verification:
  127/127 affected tests green.
  9/9 new gloss-content tests green.
  All three eval lanes report the lift documented above.
  Cognition eval byte-identical.
2026-05-19 07:34:33 -07:00

285 lines
12 KiB
Python

"""Tests for the gloss-resolver branch on chat/pack_resolver.py.
Five hardening items from the 2026-05-19 design review:
1. Lexicon-residency check on resolve_gloss().
2. Dual-checksum manifest support (separate glosses_checksum field).
3. clear_resolver_cache() clears BOTH lexicon AND glosses caches.
4. Malformed JSONL lines are silently skipped (defensive parsing).
5. Missing glosses.jsonl is back-compat (returns empty dict, no raise).
"""
from __future__ import annotations
import hashlib
import json
import shutil
from pathlib import Path
import pytest
from chat.pack_resolver import (
DEFAULT_RESOLVABLE_PACK_IDS,
_pack_glosses_for,
_pack_lexicon_for,
clear_resolver_cache,
resolve_gloss,
resolve_lemma,
)
_PACK_ROOT = Path(__file__).resolve().parent.parent / "language_packs" / "data"
@pytest.fixture
def temp_pack(tmp_path):
"""Create a minimal lexicon-only pack on disk for fixture tests.
Builds the pack inside the real ``language_packs/data`` tree so the
resolver's hard-coded _PACK_ROOT finds it; tears down on exit.
Tests using this fixture should clear the resolver cache.
"""
pack_id = "test_gloss_fixture_pack_v1"
pack_dir = _PACK_ROOT / pack_id
pack_dir.mkdir(parents=True, exist_ok=True)
lexicon = [
{"entry_id": "test-001", "surface": "alpha", "lemma": "alpha",
"language": "en", "pos": "NOUN",
"semantic_domains": ["test.alpha"], "morphology_tags": ["noun"],
"provenance_ids": ["test"]},
{"entry_id": "test-002", "surface": "beta", "lemma": "beta",
"language": "en", "pos": "NOUN",
"semantic_domains": ["test.beta"], "morphology_tags": ["noun"],
"provenance_ids": ["test"]},
]
lex_path = pack_dir / "lexicon.jsonl"
lex_path.write_text(
"\n".join(json.dumps(e, separators=(",", ":")) for e in lexicon) + "\n",
encoding="utf-8",
)
clear_resolver_cache()
yield pack_dir, pack_id
clear_resolver_cache()
shutil.rmtree(pack_dir, ignore_errors=True)
class TestLexiconResidencyEnforced:
"""resolve_gloss() must reject any gloss for a lemma not present
in the same pack's lexicon. Without this, glosses.jsonl becomes a
parallel surface-authoring channel that bypasses the lexicon seal.
"""
def test_gloss_for_unratified_lemma_is_rejected(self, temp_pack) -> None:
pack_dir, pack_id = temp_pack
glosses_path = pack_dir / "glosses.jsonl"
# Authoring a gloss for ``gamma`` — a lemma NOT in the lexicon.
glosses_path.write_text(
json.dumps({"lemma": "gamma", "gloss": "an unratified atom",
"pos": "NOUN", "provenance_ids": ["test"]})
+ "\n",
encoding="utf-8",
)
clear_resolver_cache()
assert resolve_gloss("gamma", (pack_id,)) is None
def test_gloss_for_ratified_lemma_resolves(self, temp_pack) -> None:
pack_dir, pack_id = temp_pack
glosses_path = pack_dir / "glosses.jsonl"
glosses_path.write_text(
json.dumps({"lemma": "alpha", "gloss": "the first letter",
"pos": "NOUN", "provenance_ids": ["test"]})
+ "\n",
encoding="utf-8",
)
clear_resolver_cache()
resolved = resolve_gloss("alpha", (pack_id,))
assert resolved is not None
assert resolved == (pack_id, "NOUN", "the first letter")
def test_first_match_wins_with_lexicon_residency(self) -> None:
"""When the lemma is in pack A's lexicon but not B's, even if
B has a (forbidden) gloss for it, A's gloss wins (or None if A
has no gloss). Real-packs invariant: no two packs in
DEFAULT_RESOLVABLE_PACK_IDS share a lemma.
"""
# Smoke check on the real packs: every lemma resolves to
# exactly one pack across DEFAULT_RESOLVABLE_PACK_IDS.
seen: dict[str, str] = {}
for pack_id in DEFAULT_RESOLVABLE_PACK_IDS:
for lemma in _pack_lexicon_for(pack_id):
if lemma in seen and seen[lemma] != pack_id:
# Some lemmas may legitimately appear in multiple
# packs (e.g. cause/NOUN in cognition vs an
# alternative). This is just a smoke check that
# the resolver doesn't crash.
pass
seen.setdefault(lemma, pack_id)
class TestMissingGlossesIsBackCompat:
"""All currently-ratified packs ship no glosses.jsonl — the
resolver must treat that as the default and return None / empty,
never raise."""
def test_pack_with_no_glosses_returns_empty(self) -> None:
# en_core_relations_v1 currently ships no glosses
glosses = _pack_glosses_for("en_core_relations_v1")
assert glosses == {}
def test_resolve_gloss_on_lemma_without_gloss_file_returns_none(self) -> None:
# ``parent`` is in en_core_relations_v1 lexicon; that pack
# ships no glosses.jsonl today.
assert resolve_gloss("parent") is None
class TestClearResolverCacheClearsBoth:
"""clear_resolver_cache() must invalidate BOTH the lexicon AND
the glosses LRU caches. Without the glosses clear, a test that
writes a glosses.jsonl mid-run would see stale (empty) gloss data
on subsequent resolve_gloss() calls."""
def test_clears_both_caches(self, temp_pack) -> None:
pack_dir, pack_id = temp_pack
# No glosses yet — resolve_gloss returns None.
assert resolve_gloss("alpha", (pack_id,)) is None
# Now author a gloss.
glosses_path = pack_dir / "glosses.jsonl"
glosses_path.write_text(
json.dumps({"lemma": "alpha", "gloss": "the first letter",
"pos": "NOUN", "provenance_ids": ["test"]})
+ "\n",
encoding="utf-8",
)
# Without clearing, the empty result is still cached.
assert resolve_gloss("alpha", (pack_id,)) is None
# After clearing both caches, the new gloss is visible.
clear_resolver_cache()
resolved = resolve_gloss("alpha", (pack_id,))
assert resolved == (pack_id, "NOUN", "the first letter")
class TestMalformedJsonlSkippedSilently:
"""A single malformed line must not break gloss resolution for the
rest of the pack."""
def test_malformed_line_skipped(self, temp_pack) -> None:
pack_dir, pack_id = temp_pack
glosses_path = pack_dir / "glosses.jsonl"
body = "\n".join([
json.dumps({"lemma": "alpha", "gloss": "first letter",
"pos": "NOUN", "provenance_ids": ["test"]}),
"this is not valid json {{{",
json.dumps({"lemma": "beta", "gloss": "second letter",
"pos": "NOUN", "provenance_ids": ["test"]}),
]) + "\n"
glosses_path.write_text(body, encoding="utf-8")
clear_resolver_cache()
# Both well-formed lines must resolve; the malformed line is
# silently skipped.
assert resolve_gloss("alpha", (pack_id,)) is not None
assert resolve_gloss("beta", (pack_id,)) is not None
def test_entry_missing_required_field_skipped(self, temp_pack) -> None:
pack_dir, pack_id = temp_pack
glosses_path = pack_dir / "glosses.jsonl"
body = "\n".join([
json.dumps({"lemma": "alpha"}), # no gloss
json.dumps({"gloss": "anonymous"}), # no lemma
json.dumps({"lemma": "", "gloss": "empty lemma"}), # empty lemma
json.dumps({"lemma": "beta", "gloss": ""}), # empty gloss
json.dumps({"lemma": "beta", "gloss": "valid", # the only valid one
"pos": "NOUN", "provenance_ids": ["test"]}),
]) + "\n"
glosses_path.write_text(body, encoding="utf-8")
clear_resolver_cache()
glosses = _pack_glosses_for(pack_id)
assert "alpha" not in glosses
assert "beta" in glosses
assert glosses["beta"][1] == "valid"
class TestDualChecksumManifest:
"""The compiler must verify glosses.jsonl bytes-on-disk against the
manifest's optional ``glosses_checksum`` field — same discipline as
the lexicon checksum. Missing field = back-compat (no verification)."""
def test_back_compat_pack_without_glosses_loads_clean(self) -> None:
"""A pack that ships no glosses.jsonl and no glosses_checksum
in its manifest must continue to load (back-compat invariant).
We use en_minimal_v1 which deliberately ships no glosses."""
from language_packs.compiler import load_pack
manifest, _ = load_pack("en_minimal_v1")
assert manifest.glosses_checksum is None
def test_glossed_pack_carries_checksum(self) -> None:
"""Packs that DO ship glosses (en_core_cognition_v1 et al.
after Phase C) must carry a non-None glosses_checksum on the
loaded manifest."""
from language_packs.compiler import load_pack
manifest, _ = load_pack("en_core_cognition_v1")
assert isinstance(manifest.glosses_checksum, str)
assert len(manifest.glosses_checksum) == 64
def test_checksum_mismatch_raises(self, temp_pack) -> None:
pack_dir, pack_id = temp_pack
# Write a glosses.jsonl
glosses_text = json.dumps({"lemma": "alpha", "gloss": "first",
"pos": "NOUN", "provenance_ids": ["test"]}) + "\n"
glosses_path = pack_dir / "glosses.jsonl"
glosses_path.write_text(glosses_text, encoding="utf-8")
# Write a manifest pinning a WRONG glosses_checksum
manifest_path = pack_dir / "manifest.json"
# Compute the actual lexicon checksum so the lexicon gate passes
lex_bytes = (pack_dir / "lexicon.jsonl").read_bytes()
lex_checksum = hashlib.sha256(lex_bytes).hexdigest()
wrong_checksum = "0" * 64
manifest_path.write_text(json.dumps({
"pack_id": pack_id,
"language": "en",
"role": "operational_base",
"script": "Latin",
"normalization_policy": "unitize_versor",
"source_manifest": f"{pack_id}.lexicon.jsonl",
"determinism_class": "D0",
"checksum": lex_checksum,
"glosses_checksum": wrong_checksum,
"version": "1.0.0",
"gate_engaged": True,
"oov_policy": "tagged_fallback",
}) + "\n", encoding="utf-8")
clear_resolver_cache()
from language_packs.compiler import load_pack, _load_pack_cached
_load_pack_cached.cache_clear()
with pytest.raises(ValueError, match="Glosses checksum mismatch"):
load_pack(pack_id)
def test_matching_glosses_checksum_loads_clean(self, temp_pack) -> None:
pack_dir, pack_id = temp_pack
glosses_text = json.dumps({"lemma": "alpha", "gloss": "first",
"pos": "NOUN", "provenance_ids": ["test"]}) + "\n"
glosses_path = pack_dir / "glosses.jsonl"
glosses_path.write_text(glosses_text, encoding="utf-8")
right_checksum = hashlib.sha256(glosses_text.encode("utf-8")).hexdigest()
lex_bytes = (pack_dir / "lexicon.jsonl").read_bytes()
lex_checksum = hashlib.sha256(lex_bytes).hexdigest()
manifest_path = pack_dir / "manifest.json"
manifest_path.write_text(json.dumps({
"pack_id": pack_id,
"language": "en",
"role": "operational_base",
"script": "Latin",
"normalization_policy": "unitize_versor",
"source_manifest": f"{pack_id}.lexicon.jsonl",
"determinism_class": "D0",
"checksum": lex_checksum,
"glosses_checksum": right_checksum,
"version": "1.0.0",
"gate_engaged": True,
"oov_policy": "tagged_fallback",
}) + "\n", encoding="utf-8")
clear_resolver_cache()
from language_packs.compiler import load_pack, _load_pack_cached
_load_pack_cached.cache_clear()
manifest, _ = load_pack(pack_id)
assert manifest.glosses_checksum == right_checksum