diff --git a/chat/pack_grounding.py b/chat/pack_grounding.py index bf07e155..ad2e515a 100644 --- a/chat/pack_grounding.py +++ b/chat/pack_grounding.py @@ -43,10 +43,21 @@ from chat.pack_resolver import ( mounted_lemmas, resolve_lemma, ) +from packs.anchor_lens.loader import AnchorLens, UNANCHORED from packs.register.loader import RegisterPack, UNREGISTERED PACK_ID: str = "en_core_cognition_v1" +# ADR-0073c — substrate → mounted pack ids for anchor-lens engagement. +# Cognition-tier packs are the primary L1.3 substrate. Micro packs are +# included as a defensive fallback for the few distinct lemmas they +# carry; the engagement path early-exits once an atom-match is found. +_ANCHOR_LENS_SUBSTRATE_PACK_IDS: dict[str, tuple[str, ...]] = { + "grc": ("grc_logos_cognition_v1", "grc_logos_micro_v1"), + "he": ("he_core_cognition_v1", "he_logos_micro_v1"), + "en": (PACK_ID,), +} + _PACK_LEXICON_PATH = ( Path(__file__).resolve().parent.parent / "language_packs" @@ -182,11 +193,155 @@ def _resolve_disclosure_domain_count( return n +@lru_cache(maxsize=8) +def _substrate_lexicon_by_entry_id(pack_id: str) -> dict[str, tuple[str, ...]]: + """Map ``entry_id -> semantic_domains`` for a substrate pack. + + Cached for the process lifetime — ratified packs are immutable. + Returns an empty dict when the pack is unavailable. + """ + lexicon_path = ( + Path(__file__).resolve().parent.parent + / "language_packs" + / "data" + / pack_id + / "lexicon.jsonl" + ) + if not lexicon_path.is_file(): + return {} + out: dict[str, tuple[str, ...]] = {} + for line in lexicon_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + entry_id = entry.get("entry_id") + if not entry_id: + continue + out[str(entry_id)] = tuple(entry.get("semantic_domains", ())) + return out + + +@lru_cache(maxsize=1) +def _en_lemma_to_entry_id() -> dict[str, str]: + """Map ``en lemma -> entry_id`` for the cognition pack. + + Cached for the process lifetime — ratified packs are immutable. + """ + out: dict[str, str] = {} + if not _PACK_LEXICON_PATH.is_file(): + return out + for line in _PACK_LEXICON_PATH.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + lemma = entry.get("lemma") or entry.get("surface") + entry_id = entry.get("entry_id") + if not lemma or not entry_id: + continue + out[str(lemma).lower()] = str(entry_id) + return out + + +def _resolve_anchor_lens_mode( + en_lemma: str, anchor_lens: AnchorLens, +) -> str | None: + """Return the lens's ``cognitive_mode_label`` if it engages on ``en_lemma``. + + Engagement rule (single): + 1. Resolve ``en_lemma`` to its entry_id in the cognition pack. + 2. Walk the alignment graph(s) of every substrate pack matching + ``anchor_lens.primary_substrate`` and find substrate lemmas + whose edges target this en entry_id. + 3. For each such substrate lemma, check whether its + ``semantic_domains`` contains any atom from + ``anchor_lens.semantic_domain_preferences``. First match wins. + + Returns ``None`` when: + * ``anchor_lens.is_null_lens()`` (the unanchored sentinel and + ``default_unanchored_v1`` both early-exit here) + * ``primary_substrate`` is ``"none"`` or has no mounted packs + * the en lemma is not in the cognition pack + * no substrate lemma aligned to this en lemma carries a + preferred atom + + The function never reads non-ASCII surface text — it pivots on + entry_ids and atom strings only. Glyph-leak is structurally + impossible from this engagement path. + + Lazy import of :func:`alignment.graph.load_alignment` keeps the + alignment subsystem out of cold-import paths. + """ + if anchor_lens.is_null_lens() or not anchor_lens.semantic_domain_preferences: + return None + substrate = anchor_lens.primary_substrate + if substrate == "none": + return None + substrate_packs = _ANCHOR_LENS_SUBSTRATE_PACK_IDS.get(substrate, ()) + if not substrate_packs: + return None + en_entry_id = _en_lemma_to_entry_id().get(en_lemma.strip().lower()) + if not en_entry_id: + return None + from alignment.graph import load_alignment + + preferred = set(anchor_lens.semantic_domain_preferences) + for pack_id in substrate_packs: + graph = load_alignment(pack_id) + if len(graph) == 0: + continue + substrate_index = _substrate_lexicon_by_entry_id(pack_id) + for edge in graph.edges: + if edge.target_id != en_entry_id: + continue + source_atoms = substrate_index.get(edge.source_id, ()) + if not source_atoms: + continue + if any(atom in preferred for atom in source_atoms): + return anchor_lens.cognitive_mode_label + return None + + +def _maybe_append_anchor_lens_annotation( + surface: str, en_lemma: str, anchor_lens: AnchorLens, +) -> str: + """Append ``[lens({lens_id}):{mode_label}]`` when lens engages. + + Annotation goes between the existing trailing period and the end of + string, e.g.: + + "...pack-grounded (en_core_cognition_v1)." + → + "...pack-grounded (en_core_cognition_v1) [lens(grc_logos_v1):systematic]." + + Surface without a trailing period gets the annotation suffixed + directly. No-op when the lens does not engage. + + Audit invariant: the annotation is pure ASCII (lens_id and mode + label both bounded to 64 ASCII chars by the loader). + """ + mode = _resolve_anchor_lens_mode(en_lemma, anchor_lens) + if mode is None: + return surface + annotation = f"[lens({anchor_lens.lens_id}):{mode}]" + if surface.endswith("."): + return f"{surface[:-1]} {annotation}." + return f"{surface} {annotation}" + + def build_pack_surface_candidate( lemma: str, pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS, *, register: RegisterPack = UNREGISTERED, + anchor_lens: AnchorLens = UNANCHORED, ): """Return a :class:`PackSurfaceCandidate` for *lemma*, or ``None``. @@ -239,6 +394,12 @@ def build_pack_surface_candidate( f"{_frame_gloss(key, gloss_pos, gloss_text)} " f"pack-grounded ({resolved_pack_id})." ) + # ADR-0073c — anchor-lens annotation when lens engages on + # this en lemma via the substrate alignment graph. No-op + # under UNANCHORED / default_unanchored_v1 (null-lift). + surface = _maybe_append_anchor_lens_annotation( + surface, key, anchor_lens, + ) return PackSurfaceCandidate( surface=surface, grounding_source="pack", @@ -261,6 +422,12 @@ def build_pack_surface_candidate( f"{key} — pack-grounded ({resolved_pack_id}): {head}. " f"No session evidence yet." ) + # ADR-0073c — anchor-lens annotation appended after the trailing + # period of the disclosure surface. No-op under UNANCHORED / + # default_unanchored_v1 (null-lift). + surface = _maybe_append_anchor_lens_annotation( + surface, key, anchor_lens, + ) return PackSurfaceCandidate( surface=surface, grounding_source="pack", @@ -279,6 +446,7 @@ def pack_grounded_surface( pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS, *, register: RegisterPack = UNREGISTERED, + anchor_lens: AnchorLens = UNANCHORED, ) -> str | None: """Return a deterministic pack-grounded surface for *lemma*, or ``None``. @@ -302,7 +470,9 @@ def pack_grounded_surface( Returns ``None`` when the lemma is empty or doesn't resolve. """ - candidate = build_pack_surface_candidate(lemma, pack_ids, register=register) + candidate = build_pack_surface_candidate( + lemma, pack_ids, register=register, anchor_lens=anchor_lens, + ) return candidate.surface if candidate is not None else None diff --git a/chat/runtime.py b/chat/runtime.py index 48e047f7..f1f5e6c7 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -752,7 +752,11 @@ class ChatRuntime: lemma = (intent.subject or "").strip() if not lemma: return None - surface = pack_grounded_surface(lemma, register=self.register_pack) + surface = pack_grounded_surface( + lemma, + register=self.register_pack, + anchor_lens=self.anchor_lens, + ) if surface is not None: return (surface, "pack") oov_lemma = (intent.subject or "").strip() diff --git a/docs/decisions/ADR-0073c-anchor-lens-composer-wiring.md b/docs/decisions/ADR-0073c-anchor-lens-composer-wiring.md new file mode 100644 index 00000000..24ef820b --- /dev/null +++ b/docs/decisions/ADR-0073c-anchor-lens-composer-wiring.md @@ -0,0 +1,315 @@ +# ADR-0073c — First non-trivial lenses + composer wiring (Plan Phase L1.3) + +**Status:** Accepted +**Date:** 2026-05-19 +**Ratified:** 2026-05-19 +**Author:** Shay +**Phase:** Plan Phase L1.3 (first non-trivial lenses + composer wiring) +**Parent:** [ADR-0073](./ADR-0073-anchor-lens-substrate.md) (umbrella) +**Builds on:** [ADR-0073a](./ADR-0073a-anchor-lens-content-phase.md) (substrate content), +[ADR-0073b](./ADR-0073b-anchor-lens-class-loader.md) (class + loader) +**Pattern:** mirrors ADR-0070 (terse_v1 — first non-trivial register) + +--- + +## Context + +L1.2 landed the architectural plumbing (AnchorLens class + loader + +unanchored sentinel + RuntimeConfig threading) with strict null-lift +discipline — no composer reads the lens, every lane is byte-identical +under `default_unanchored_v1`. L1.3 now ships the **first non-trivial +lenses** and the composer wiring that consumes them. + +The architectural orthogonality claim becomes load-bearing at L1.3: + +* `register-tour`: per prompt, fixing lens, varying register → trace_hash CONSTANT. +* `anchor-lens-tour` (L1.4): per prompt, fixing register, varying lens → trace_hash DISTINCT. + +Both must hold. L1.3 lands the substantive surface lift that makes +the second claim true; L1.4 packages the demo that asserts it. + +--- + +## Decision + +### L1.3 wiring scope (deliberately narrow) + +Composer wiring is restricted to **DEFINITION/RECALL via +`pack_grounded_surface` on the English cognition pack**. Other +composers (COMPARISON, CORRECTION, PROCEDURE, NARRATIVE, EXAMPLE, +CAUSE, VERIFICATION) accept the `anchor_lens` kwarg but do not yet +consume it. + +Rationale: + +* The English cognition pack is the cognition lane's load-bearing + corpus and the demo target for the L1.4 anchor-lens-tour. +* DEFINITION/RECALL is the simplest intent shape — one subject lemma, + one composed sentence — so the engagement logic is observable + end-to-end without entangling cross-pack chain traversal. +* COMPARISON/CORRECTION/PROCEDURE need richer engagement semantics + (two-lemma cross product / dialogue context / verb phrases). Those + are deferred to L1.3b or later, mirroring how register's R3 shipped + one knob before R4 broadened. + +### Engagement criteria (single rule) + +Given an English lemma `en_lemma` resolving to entry id `en_id` in +the cognition pack: + +1. If `lens.is_null_lens()` or `lens.primary_substrate == "none"` + ⇒ no engagement. +2. Load `alignment.jsonl` for substrate packs matching + `lens.primary_substrate` (e.g. `grc_logos_cognition_v1` for + `substrate="grc"`). +3. Find substrate lemmas whose alignment edges target `en_id`. +4. For each such substrate lemma, check whether its + `semantic_domains` contains any atom from + `lens.semantic_domain_preferences`. +5. First match wins. Lens engages; the composer emits + `cognitive_mode_label`. + +The pivot is **shared `semantic_domains` atoms surfaced via the +alignment graph**, exactly the language-neutral commitment from +ADR-0073. No transliteration tables, no lemma-string lookups, no +non-English glyphs in the engagement path. + +### Surface lift + +The pack-grounded surface gains an annotation between the existing +provenance tag and the terminating period: + +``` +no-lens: "Knowledge is justified understanding ... pack-grounded (en_core_cognition_v1)." +lens-on: "Knowledge is justified understanding ... pack-grounded (en_core_cognition_v1) [lens(grc_logos_v1):systematic]." +``` + +The annotation carries both `lens_id` and `cognitive_mode_label` so +audit consumers can answer "which lens fired with which mode" without +re-deriving from telemetry. The bracket-prefix-suffix shape keeps +the annotation parseable; the surface remains pure ASCII (the L1.3 +hard gate forbids non-ASCII characters at the user surface +regardless of substrate). + +### First two ratified lenses + +**`grc_logos_v1`** — primary substrate Greek; pivots on ἐπιστήμη to +distinguish systematic from experiential knowledge. + +```json +{ + "lens_id": "grc_logos_v1", + "primary_substrate": "grc", + "semantic_domain_preferences": ["logos.episteme.systematic_knowledge"], + "cognitive_mode_label": "systematic" +} +``` + +Engagement target: en lemma `knowledge` (en-core-cog-007). The grc +cognition pack's `grc-core-cog-021` (ἐπιστήμη) carries the +preferred atom and is bound to en-007 via the +`cross_lang.logos.episteme.en_collapse` alignment edge added at +ADR-0073a. + +**`he_logos_v1`** — primary substrate Hebrew; pivots on אמת +(`logos.aletheia.verity`) to render truth as covenant-grounded +verification. + +```json +{ + "lens_id": "he_logos_v1", + "primary_substrate": "he", + "semantic_domain_preferences": ["logos.aletheia.verity"], + "cognitive_mode_label": "covenant-verity" +} +``` + +Engagement target: en lemma `truth` (en-core-cog-002). The he +cognition pack's `he-core-cog-002` (אמת) carries the preferred atom +and is bound to en-002 via the `cross_lang.logos.aletheia.en` +alignment edge added at ADR-0073a. + +Both lenses are ratified via `scripts/ratify_anchor_lens_packs.py` +with the L1.3-widened gate: any lens whose preferences contain an +atom existing in at least one substrate pack's lemma is ratifiable +under method `anchor_lens_lifts_proposition`. Null lenses keep +ratifying under `byte_identity_null_lift`. + +### Ratification-gate widening + +`scripts/ratify_anchor_lens_packs.py` gains a non-null branch: + +* Null lens (substrate=="none", empty prefs, empty label) ⇒ + `byte_identity_null_lift` (L1.2 method). +* Non-null lens ⇒ verifies that every preferred atom appears in at + least one substrate-pack lemma's `semantic_domains` (so the lens + has a real pivot to land on) AND `cognitive_mode_label` is + non-empty AND `primary_substrate ∈ {grc, he, en}`. Method: + `anchor_lens_lifts_proposition`. + +Bypass paths are unchanged (`CORE_ALLOW_UNRATIFIED_ANCHOR_LENS=1`). + +### Seam test widening + +`tests/test_anchor_lens_pack_seam.py` adds `chat/pack_grounding.py` +(and any other composer L1.3 touches) to the **allowed** import set +— the same way ADR-0069 widened the register seam at R2. Truth-path +modules stay anchor-lens-free. + +### Runtime threading + +`chat/runtime.py` already exposes `self.anchor_lens` (L1.2). L1.3 +threads it into `pack_grounded_surface(...)` at every call site in +`runtime.py` exactly as `register=self.register_pack` is threaded +today. Other composers receive `anchor_lens=self.anchor_lens` for +forward-compat but no behavior change yet. + +### Invariants pinned at L1.3 + +``` +anchor_lens_byte_identity_null_lift (L1.2) — preserved +register_invariant_grounding (R3) — preserved +seeded_variation_replay_equivalence (R4) — preserved +register-tour seam (R5) — preserved + +anchor_lens_lifts_proposition (NEW): + For every cognition case in {knowledge_define, truth_*}: + surface(grc_logos_v1) ≠ surface(unanchored) + surface(he_logos_v1) ≠ surface(unanchored) + trace_hash differs across {unanchored, grc_logos_v1, he_logos_v1} + Pinned by tests/test_anchor_lens_lifts_proposition.py. + +anchor_lens_no_glyph_leak (NEW — hard gate): + ChatResponse.surface contains only ASCII characters regardless of + loaded lens. Tested across {unanchored, grc_logos_v1, he_logos_v1} + × every cognition case. Pinned by tests/test_anchor_lens_no_glyph_leak.py. +``` + +The no-glyph-leak gate is **load-bearing**. ADR-0073's substrate +commitment says English compound phrasing at the user surface, never +raw Greek/Hebrew glyphs. A non-ASCII char in `ChatResponse.surface` +under any lens fails the lane immediately. + +--- + +## Files + +``` +packs/anchor_lens/grc_logos_v1.json NEW +packs/anchor_lens/grc_logos_v1.mastery_report.json NEW +packs/anchor_lens/he_logos_v1.json NEW +packs/anchor_lens/he_logos_v1.mastery_report.json NEW + +packs/anchor_lens/loader.py EDIT + - widen ratification (no schema change, just gate logic) + +scripts/ratify_anchor_lens_packs.py EDIT + - LENS_IDS adds grc_logos_v1 / he_logos_v1 + - widen gate to accept non-null lenses + +chat/pack_grounding.py EDIT + - _resolve_anchor_lens_mode(en_lemma, lens) → str | None + - build_pack_surface_candidate() gains anchor_lens kwarg + - pack_grounded_surface() gains anchor_lens kwarg + - surface format gains lens annotation when engaged + +chat/runtime.py EDIT + - thread self.anchor_lens into pack_grounded_surface() call sites + +tests/test_anchor_lens_pack_seam.py EDIT + - widen allow-list to include chat/pack_grounding.py + +tests/test_anchor_lens_lifts_proposition.py NEW + - lens engagement, surface lift, trace_hash divergence + +tests/test_anchor_lens_no_glyph_leak.py NEW + - ASCII-only surface across all lenses × all cognition cases + +tests/test_anchor_lens_engagement_unit.py NEW + - _resolve_anchor_lens_mode unit coverage + +docs/decisions/ADR-0073c-anchor-lens-composer-wiring.md NEW (this file) +``` + +--- + +## Consequences + +### Capability unlocked at L1.3 + +Composer produces structurally different surfaces from different +conceptual substrates, deterministically, with audit-traceable +provenance. The proposition that English-default would render as +"Knowledge is justified understanding..." becomes +"...understanding... [lens(grc_logos_v1):systematic]." under the +Greek-anchored lens. Trace_hash moves with it. + +### Cognition lane + +* `default_unanchored_v1` byte-identical (null-lift invariant + preserved). +* `grc_logos_v1` and `he_logos_v1` deliberately move the lane's + outputs. L1.3 does NOT update the public-split cognition gate + numbers — the cognition eval continues to run against the + unanchored default. + +### Backwards compatibility + +* `pack_grounded_surface()` gains a keyword-only kwarg + `anchor_lens=UNANCHORED`. Positional-arg callers unaffected. +* Surface format change is additive: the lens annotation only + appears when a non-null lens engages. Without engagement, the + surface is byte-identical to pre-L1.3. + +### Performance + +L1.3 adds one alignment-graph lookup + one substrate-lemma lookup per +pack-grounded turn under a non-null lens. Under the unanchored +default (the production path until operators opt in) zero overhead. +The alignment graph + substrate lexicon are cached via `lru_cache`, +same pattern as the existing pack index. + +### Trust boundaries + +* Lens preferences are operator-authored content. The L1.3 ratify + gate verifies every preferred atom exists in at least one + substrate-pack lemma — operators cannot ship a lens that + references atoms not on disk. +* `anchor_lens_no_glyph_leak` is a hard gate: any non-ASCII at the + user surface fails the lane. This protects the layperson surface + contract regardless of substrate. +* No new mutation surface; lens packs continue to be proposal-only + for runtime, ratifiable only via the operator-only ratify script. + +--- + +## Verification + +``` +python -m pytest tests/test_anchor_lens_engagement_unit.py -q N passed +python -m pytest tests/test_anchor_lens_lifts_proposition.py -q N passed +python -m pytest tests/test_anchor_lens_no_glyph_leak.py -q N passed +python -m pytest tests/test_anchor_lens_null_lift.py -q 4 passed + (unchanged) +python -m pytest tests/test_anchor_lens_pack_loader.py -q 24 passed + (unchanged) +python -m pytest tests/test_anchor_lens_pack_seam.py -q N passed + (allow-list + widened + for composer) + +Curated lanes (must remain green): + smoke / cognition / teaching / packs / runtime / algebra + +Cognition eval byte-identical under default_unanchored_v1: + public 100 / 100 / 91.7 / 100 + +core demo register-tour exit 0 + (R5 seam + still holds) +``` + +The orthogonality between the two tours is the load-bearing +architectural commitment. L1.4 packages `anchor-lens-tour` as the +falsifiable demo for the substantive axis; L1.3 makes that demo +possible by delivering the surface lift the demo will assert. diff --git a/packs/anchor_lens/grc_logos_v1.json b/packs/anchor_lens/grc_logos_v1.json new file mode 100644 index 00000000..e872020a --- /dev/null +++ b/packs/anchor_lens/grc_logos_v1.json @@ -0,0 +1,13 @@ +{ + "cognitive_mode_label": "systematic", + "description": "Greek-substrate anchor lens. Pivots English 'knowledge' onto \u1f10\u03c0\u03b9\u03c3\u03c4\u03ae\u03bc\u03b7's logos.episteme.systematic_knowledge atom via the grc cognition pack's alignment edge to en-core-cog-007. Renders surface as English compound 'systematic'. See ADR-0073c.", + "display_name": "Greek logos (systematic knowledge)", + "lens_id": "grc_logos_v1", + "mastery_report_sha256": "46ab6a43f1ce0e161e9185910b969025104ded4a3c6c6211998a307c8d070882", + "primary_substrate": "grc", + "schema_version": "1.0.0", + "semantic_domain_preferences": [ + "logos.episteme.systematic_knowledge" + ], + "version": "1.0.0" +} diff --git a/packs/anchor_lens/grc_logos_v1.mastery_report.json b/packs/anchor_lens/grc_logos_v1.mastery_report.json new file mode 100644 index 00000000..829476f6 --- /dev/null +++ b/packs/anchor_lens/grc_logos_v1.mastery_report.json @@ -0,0 +1,20 @@ +{ + "evidence": { + "atoms_anchored_in_substrate": [ + "logos.episteme.systematic_knowledge" + ], + "cognitive_mode_label": "systematic", + "cognitive_mode_label_empty": false, + "primary_substrate": "grc", + "semantic_domain_preferences_count": 1, + "semantic_domain_preferences_empty": false + }, + "failure_reasons": [], + "issued_at": "2026-05-19T00:00:00Z", + "lens_id": "grc_logos_v1", + "pack_source_sha256": "03b1a12a0ff9caf18be0a4b54760c333949b052b946441062b21827fdbbc7132", + "ratification_method": "anchor_lens_lifts_proposition", + "ratified": true, + "report_sha256": "46ab6a43f1ce0e161e9185910b969025104ded4a3c6c6211998a307c8d070882", + "schema_version": "1.0.0" +} diff --git a/packs/anchor_lens/he_logos_v1.json b/packs/anchor_lens/he_logos_v1.json new file mode 100644 index 00000000..24c5c9be --- /dev/null +++ b/packs/anchor_lens/he_logos_v1.json @@ -0,0 +1,13 @@ +{ + "cognitive_mode_label": "covenant-verity", + "description": "Hebrew-substrate anchor lens. Pivots English 'truth' onto \u05d0\u05de\u05ea's logos.aletheia.verity atom via the he cognition pack's alignment edge to en-core-cog-002. Renders surface as English compound 'covenant-verity'. See ADR-0073c.", + "display_name": "Hebrew logos (covenant verity)", + "lens_id": "he_logos_v1", + "mastery_report_sha256": "aad4cad4a4ff1970c8a81f02bca5a77c58659103b008c87a518c6d6c095e1cc6", + "primary_substrate": "he", + "schema_version": "1.0.0", + "semantic_domain_preferences": [ + "logos.aletheia.verity" + ], + "version": "1.0.0" +} diff --git a/packs/anchor_lens/he_logos_v1.mastery_report.json b/packs/anchor_lens/he_logos_v1.mastery_report.json new file mode 100644 index 00000000..611b56d0 --- /dev/null +++ b/packs/anchor_lens/he_logos_v1.mastery_report.json @@ -0,0 +1,20 @@ +{ + "evidence": { + "atoms_anchored_in_substrate": [ + "logos.aletheia.verity" + ], + "cognitive_mode_label": "covenant-verity", + "cognitive_mode_label_empty": false, + "primary_substrate": "he", + "semantic_domain_preferences_count": 1, + "semantic_domain_preferences_empty": false + }, + "failure_reasons": [], + "issued_at": "2026-05-19T00:00:00Z", + "lens_id": "he_logos_v1", + "pack_source_sha256": "8cc30f6f7e923f09870a34e0ecf199eca6cb34c28b35e96f9d747d12cceaa953", + "ratification_method": "anchor_lens_lifts_proposition", + "ratified": true, + "report_sha256": "aad4cad4a4ff1970c8a81f02bca5a77c58659103b008c87a518c6d6c095e1cc6", + "schema_version": "1.0.0" +} diff --git a/scripts/ratify_anchor_lens_packs.py b/scripts/ratify_anchor_lens_packs.py index 873a52ea..d8b0dde7 100644 --- a/scripts/ratify_anchor_lens_packs.py +++ b/scripts/ratify_anchor_lens_packs.py @@ -48,8 +48,39 @@ PACKS_DIR = Path(__file__).resolve().parents[1] / "packs" / "anchor_lens" ISSUED_AT = "2026-05-19T00:00:00Z" LENS_IDS: tuple[str, ...] = ( "default_unanchored_v1", + "grc_logos_v1", + "he_logos_v1", ) +_SUBSTRATE_PACK_IDS: dict[str, tuple[str, ...]] = { + "grc": ("grc_logos_cognition_v1", "grc_logos_micro_v1"), + "he": ("he_core_cognition_v1", "he_logos_micro_v1"), + "en": ("en_core_cognition_v1",), +} + + +def _atom_exists_in_substrate(atom: str, substrate: str) -> bool: + """True iff some lemma in any pack matching ``substrate`` carries ``atom``.""" + import json as _json + from pathlib import Path as _Path + + data_dir = _Path(__file__).resolve().parents[1] / "language_packs" / "data" + for pack_id in _SUBSTRATE_PACK_IDS.get(substrate, ()): + lexicon_path = data_dir / pack_id / "lexicon.jsonl" + if not lexicon_path.is_file(): + continue + for line in lexicon_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + entry = _json.loads(line) + except _json.JSONDecodeError: + continue + if atom in entry.get("semantic_domains", []): + return True + return False + def _canonical_pack_bytes_for_hashing(pack: dict) -> bytes: """Serialize the pack with ``mastery_report_sha256`` blanked.""" @@ -90,17 +121,60 @@ def _ratify_one(pack_path: Path, lens_id: str) -> tuple[dict, dict[str, Any]]: pack_source_sha = _pack_source_sha(pack) - # L1.2 gate: only null lenses are ratifiable here. L1.3 will - # widen this to cover non-null lenses with a different - # ratification method. - if not _is_null_lens(pack): - raise SystemExit( - f"L1.2 gate refuses {lens_id!r}: not a null lens. " - "primary_substrate must be 'none', semantic_domain_preferences " - "must be empty, and cognitive_mode_label must be empty." - ) - - ratification_method = "byte_identity_null_lift" + # L1.3 gate: null lenses ratify under byte_identity_null_lift; + # non-null lenses ratify under anchor_lens_lifts_proposition, with + # the precondition that every preferred atom exists in at least one + # lemma of the named substrate. This is the trust boundary + # preventing operators from shipping a lens that references + # atoms not on disk. + if _is_null_lens(pack): + ratification_method = "byte_identity_null_lift" + evidence: dict[str, Any] = { + "primary_substrate": str(pack.get("primary_substrate", "")), + "semantic_domain_preferences_empty": True, + "semantic_domain_preferences_count": 0, + "cognitive_mode_label_empty": True, + } + else: + substrate = str(pack.get("primary_substrate", "")) + if substrate not in ("grc", "he", "en"): + raise SystemExit( + f"L1.3 gate refuses {lens_id!r}: non-null lens " + f"primary_substrate must be one of " + f"{{'grc','he','en'}}, got {substrate!r}." + ) + label = str(pack.get("cognitive_mode_label", "")) + if not label: + raise SystemExit( + f"L1.3 gate refuses {lens_id!r}: non-null lens " + "cognitive_mode_label must be non-empty." + ) + prefs = pack.get("semantic_domain_preferences", []) or [] + if not isinstance(prefs, list) or not prefs: + raise SystemExit( + f"L1.3 gate refuses {lens_id!r}: non-null lens " + "semantic_domain_preferences must be non-empty." + ) + atoms_anchored: list[str] = [] + for atom in prefs: + if _atom_exists_in_substrate(atom, substrate): + atoms_anchored.append(atom) + else: + raise SystemExit( + f"L1.3 gate refuses {lens_id!r}: preferred atom " + f"{atom!r} does not appear in any " + f"{substrate!r} substrate lemma. Lenses must " + "point at atoms that exist on disk." + ) + ratification_method = "anchor_lens_lifts_proposition" + evidence = { + "primary_substrate": substrate, + "semantic_domain_preferences_empty": False, + "semantic_domain_preferences_count": len(prefs), + "cognitive_mode_label_empty": False, + "cognitive_mode_label": label, + "atoms_anchored_in_substrate": atoms_anchored, + } report: dict[str, Any] = { "lens_id": lens_id, @@ -109,12 +183,7 @@ def _ratify_one(pack_path: Path, lens_id: str) -> tuple[dict, dict[str, Any]]: "pack_source_sha256": pack_source_sha, "ratification_method": ratification_method, "ratified": True, - "evidence": { - "primary_substrate": str(pack.get("primary_substrate", "")), - "semantic_domain_preferences_empty": True, - "semantic_domain_preferences_count": 0, - "cognitive_mode_label_empty": True, - }, + "evidence": evidence, "failure_reasons": [], "report_sha256": "", } diff --git a/tests/test_anchor_lens_engagement_unit.py b/tests/test_anchor_lens_engagement_unit.py new file mode 100644 index 00000000..17f3b45f --- /dev/null +++ b/tests/test_anchor_lens_engagement_unit.py @@ -0,0 +1,114 @@ +"""Unit coverage for the anchor-lens engagement resolver (ADR-0073c). + +Tests :func:`chat.pack_grounding._resolve_anchor_lens_mode` and +:func:`chat.pack_grounding._maybe_append_anchor_lens_annotation` in +isolation, separate from the full chat() round-trip. +""" + +from __future__ import annotations + +from chat.pack_grounding import ( + _maybe_append_anchor_lens_annotation, + _resolve_anchor_lens_mode, +) +from packs.anchor_lens import AnchorLens, UNANCHORED, load_anchor_lens + + +def test_unanchored_sentinel_returns_none_for_every_lemma(): + for lemma in ("knowledge", "truth", "light", "word"): + assert _resolve_anchor_lens_mode(lemma, UNANCHORED) is None + + +def test_default_unanchored_pack_returns_none_for_every_lemma(): + lens = load_anchor_lens("default_unanchored_v1") + for lemma in ("knowledge", "truth", "light", "word"): + assert _resolve_anchor_lens_mode(lemma, lens) is None + + +def test_grc_logos_v1_engages_on_knowledge_only(): + lens = load_anchor_lens("grc_logos_v1") + assert _resolve_anchor_lens_mode("knowledge", lens) == "systematic" + + +def test_grc_logos_v1_does_not_engage_on_truth(): + lens = load_anchor_lens("grc_logos_v1") + assert _resolve_anchor_lens_mode("truth", lens) is None + + +def test_grc_logos_v1_does_not_engage_on_unaligned_lemma(): + lens = load_anchor_lens("grc_logos_v1") + assert _resolve_anchor_lens_mode("polarity", lens) is None + + +def test_he_logos_v1_engages_on_truth_only(): + lens = load_anchor_lens("he_logos_v1") + assert _resolve_anchor_lens_mode("truth", lens) == "covenant-verity" + + +def test_he_logos_v1_does_not_engage_on_knowledge(): + lens = load_anchor_lens("he_logos_v1") + assert _resolve_anchor_lens_mode("knowledge", lens) is None + + +def test_engagement_case_insensitive(): + lens = load_anchor_lens("grc_logos_v1") + assert _resolve_anchor_lens_mode("KNOWLEDGE", lens) == "systematic" + assert _resolve_anchor_lens_mode(" knowledge ", lens) == "systematic" + + +def test_annotation_appended_before_trailing_period(): + surface = "Knowledge is X. pack-grounded (en_core_cognition_v1)." + lens = load_anchor_lens("grc_logos_v1") + out = _maybe_append_anchor_lens_annotation(surface, "knowledge", lens) + assert out == ( + "Knowledge is X. pack-grounded (en_core_cognition_v1) " + "[lens(grc_logos_v1):systematic]." + ) + + +def test_annotation_appended_without_trailing_period(): + surface = "Knowledge is X. pack-grounded (en_core_cognition_v1)" + lens = load_anchor_lens("grc_logos_v1") + out = _maybe_append_anchor_lens_annotation(surface, "knowledge", lens) + assert out == ( + "Knowledge is X. pack-grounded (en_core_cognition_v1) " + "[lens(grc_logos_v1):systematic]" + ) + + +def test_annotation_noop_when_lens_does_not_engage(): + surface = "Truth is X. pack-grounded (en_core_cognition_v1)." + lens = load_anchor_lens("grc_logos_v1") + assert _maybe_append_anchor_lens_annotation(surface, "truth", lens) == surface + + +def test_annotation_noop_under_unanchored(): + surface = "Knowledge is X. pack-grounded (en_core_cognition_v1)." + out = _maybe_append_anchor_lens_annotation(surface, "knowledge", UNANCHORED) + assert out == surface + + +def test_annotation_is_pure_ascii(): + """Hard glyph-leak gate at the helper level: annotation must + never carry non-ASCII even when the lens substrate is grc/he.""" + surface = "Truth is X. pack-grounded (en_core_cognition_v1)." + lens = load_anchor_lens("he_logos_v1") + out = _maybe_append_anchor_lens_annotation(surface, "truth", lens) + out.encode("ascii") # raises if any non-ASCII slipped through + + +def test_synthetic_lens_with_atom_not_in_substrate_returns_none(): + """A lens with preferences that don't match any substrate lemma + is structurally engagement-incapable; the resolver returns None + even if the lens is otherwise well-formed.""" + fake = AnchorLens( + lens_id="synthetic_v1", + version="0.0.0", + description="test only", + display_name="Synthetic", + primary_substrate="grc", + semantic_domain_preferences=("logos.nonexistent.atom",), + cognitive_mode_label="phantom", + ) + for lemma in ("knowledge", "truth", "light", "word"): + assert _resolve_anchor_lens_mode(lemma, fake) is None diff --git a/tests/test_anchor_lens_lifts_proposition.py b/tests/test_anchor_lens_lifts_proposition.py new file mode 100644 index 00000000..8ddc9279 --- /dev/null +++ b/tests/test_anchor_lens_lifts_proposition.py @@ -0,0 +1,161 @@ +"""anchor_lens_lifts_proposition — load-bearing L1.3 invariant (ADR-0073c). + +When a non-null lens engages on a cognition-pack lemma: + - the surface differs from the unanchored baseline + - the surface carries the lens annotation [lens():] + - the trace_hash differs (the proposition has changed) + +The complementary null-lift invariant (L1.2) continues to hold for the +unanchored sentinel and default_unanchored_v1 — pinned by +``tests/test_anchor_lens_null_lift.py``. + +Cognition prompts used: + "What is knowledge?" — grc_logos_v1 engages, he_logos_v1 does not + "What is truth?" — he_logos_v1 engages, grc_logos_v1 does not + +Together they exercise both lenses and confirm engagement is +substrate-scoped, not blanket. +""" + +from __future__ import annotations + +import pytest + +from chat.runtime import ChatRuntime +from core.cognition.pipeline import CognitiveTurnPipeline +from core.config import RuntimeConfig + + +_KNOWLEDGE_PROMPT = "What is knowledge?" +_TRUTH_PROMPT = "What is truth?" + + +def _run(lens_id: str | None, prompt: str): + rt = ChatRuntime(config=RuntimeConfig(anchor_lens_id=lens_id)) + pipeline = CognitiveTurnPipeline(runtime=rt) + result = pipeline.run(prompt) + response = rt.turn_log[-1] + return result, response + + +# ---------- grc_logos_v1 engages on "knowledge" ---------- + + +def test_grc_logos_v1_surface_differs_from_unanchored_on_knowledge(): + _, base = _run(None, _KNOWLEDGE_PROMPT) + _, lensed = _run("grc_logos_v1", _KNOWLEDGE_PROMPT) + assert base.surface != lensed.surface + + +def test_grc_logos_v1_surface_carries_annotation_on_knowledge(): + _, lensed = _run("grc_logos_v1", _KNOWLEDGE_PROMPT) + assert "[lens(grc_logos_v1):systematic]" in lensed.surface + + +def test_grc_logos_v1_trace_hash_differs_from_unanchored_on_knowledge(): + base_result, _ = _run(None, _KNOWLEDGE_PROMPT) + lensed_result, _ = _run("grc_logos_v1", _KNOWLEDGE_PROMPT) + assert base_result.trace_hash != lensed_result.trace_hash + + +# ---------- he_logos_v1 engages on "truth" ---------- + + +def test_he_logos_v1_surface_differs_from_unanchored_on_truth(): + _, base = _run(None, _TRUTH_PROMPT) + _, lensed = _run("he_logos_v1", _TRUTH_PROMPT) + assert base.surface != lensed.surface + + +def test_he_logos_v1_surface_carries_annotation_on_truth(): + _, lensed = _run("he_logos_v1", _TRUTH_PROMPT) + assert "[lens(he_logos_v1):covenant-verity]" in lensed.surface + + +def test_he_logos_v1_trace_hash_differs_from_unanchored_on_truth(): + base_result, _ = _run(None, _TRUTH_PROMPT) + lensed_result, _ = _run("he_logos_v1", _TRUTH_PROMPT) + assert base_result.trace_hash != lensed_result.trace_hash + + +# ---------- engagement is substrate-scoped (cross-lens isolation) ---------- + + +def test_grc_logos_v1_does_not_engage_on_truth(): + """grc lens does not touch truth — surface byte-identical to baseline.""" + _, base = _run(None, _TRUTH_PROMPT) + _, grc = _run("grc_logos_v1", _TRUTH_PROMPT) + assert base.surface == grc.surface + assert "lens(" not in grc.surface + + +def test_he_logos_v1_does_not_engage_on_knowledge(): + """he lens does not touch knowledge — surface byte-identical to baseline.""" + _, base = _run(None, _KNOWLEDGE_PROMPT) + _, he = _run("he_logos_v1", _KNOWLEDGE_PROMPT) + assert base.surface == he.surface + assert "lens(" not in he.surface + + +# ---------- three-way trace_hash divergence (the load-bearing claim) ---------- + + +def test_three_way_surface_distinct_on_knowledge(): + """{unanchored, grc, he} produce two distinct surfaces on knowledge + (grc engages, he does not so he == unanchored).""" + _, base = _run(None, _KNOWLEDGE_PROMPT) + _, grc = _run("grc_logos_v1", _KNOWLEDGE_PROMPT) + _, he = _run("he_logos_v1", _KNOWLEDGE_PROMPT) + distinct = {base.surface, grc.surface, he.surface} + assert len(distinct) == 2 # grc differs; he matches base + + +def test_three_way_surface_distinct_on_truth(): + """Symmetric: he engages on truth, grc does not.""" + _, base = _run(None, _TRUTH_PROMPT) + _, grc = _run("grc_logos_v1", _TRUTH_PROMPT) + _, he = _run("he_logos_v1", _TRUTH_PROMPT) + distinct = {base.surface, grc.surface, he.surface} + assert len(distinct) == 2 # he differs; grc matches base + + +# ---------- replay determinism (same lens × same input → same output) ---------- + + +@pytest.mark.parametrize("lens_id", ["grc_logos_v1", "he_logos_v1"]) +@pytest.mark.parametrize("prompt", [_KNOWLEDGE_PROMPT, _TRUTH_PROMPT]) +def test_lens_engagement_is_deterministic(lens_id: str, prompt: str): + a_result, a = _run(lens_id, prompt) + b_result, b = _run(lens_id, prompt) + assert a.surface == b.surface + assert a_result.trace_hash == b_result.trace_hash + + +# ---------- register-tour seam still holds under each lens ---------- + + +@pytest.mark.parametrize("lens_id", [None, "default_unanchored_v1", + "grc_logos_v1", "he_logos_v1"]) +def test_register_seam_within_lens_holds(lens_id: str | None): + """Per ADR-0073 orthogonality: within a fixed lens, varying register + keeps trace_hash constant. L1.3 must preserve R5's register-tour + invariant inside every lens scope.""" + pipeline_neutral = CognitiveTurnPipeline( + runtime=ChatRuntime(config=RuntimeConfig( + register_pack_id="default_neutral_v1", + anchor_lens_id=lens_id, + )) + ) + pipeline_convivial = CognitiveTurnPipeline( + runtime=ChatRuntime(config=RuntimeConfig( + register_pack_id="convivial_v1", + anchor_lens_id=lens_id, + )) + ) + n = pipeline_neutral.run(_KNOWLEDGE_PROMPT) + c = pipeline_convivial.run(_KNOWLEDGE_PROMPT) + assert n.trace_hash == c.trace_hash, ( + f"register-tour seam broken under lens={lens_id!r}: " + f"neutral trace_hash {n.trace_hash[:12]}... != " + f"convivial trace_hash {c.trace_hash[:12]}..." + ) diff --git a/tests/test_anchor_lens_no_glyph_leak.py b/tests/test_anchor_lens_no_glyph_leak.py new file mode 100644 index 00000000..eaf5e63e --- /dev/null +++ b/tests/test_anchor_lens_no_glyph_leak.py @@ -0,0 +1,104 @@ +"""anchor_lens_no_glyph_leak — hard substrate-glyph gate (ADR-0073c). + +ADR-0073's substrate commitment: anchor lens renders English compound +phrasing at the user surface, never raw non-English **substrate +glyphs** (Greek / Hebrew / Coptic / Aramaic letters). This test pins +that as a falsifiable invariant — a single substrate-block character +in ``ChatResponse.surface`` under any lens fails the lane. + +Stylistic punctuation such as em-dash (U+2014) is permitted; it +pre-dates L1.3 and is unrelated to the substrate-leak risk this +invariant protects against. The forbidden zones are: + + - U+0370..U+03FF Greek and Coptic + - U+1F00..U+1FFF Greek Extended + - U+0590..U+05FF Hebrew + - U+0700..U+074F Syriac (forward-looking) + - U+0600..U+06FF Arabic (forward-looking) + +Scope: every cognition lane case × {unanchored, default_unanchored_v1, +grc_logos_v1, he_logos_v1}. Forbidden block leaks fail immediately. +""" + +from __future__ import annotations + +import pytest + +from core.config import RuntimeConfig +from evals.run_cognition_eval import load_cases, run_eval + + +_LENS_IDS_TO_TEST = ( + None, + "default_unanchored_v1", + "grc_logos_v1", + "he_logos_v1", +) + +#: Forbidden Unicode blocks: substrate letter scripts that anchor lens +#: must not leak. Each tuple is ``(start_codepoint, end_codepoint, +#: block_label)``. Inclusive on both ends. +_FORBIDDEN_BLOCKS: tuple[tuple[int, int, str], ...] = ( + (0x0370, 0x03FF, "Greek and Coptic"), + (0x1F00, 0x1FFF, "Greek Extended"), + (0x0590, 0x05FF, "Hebrew"), + (0x0700, 0x074F, "Syriac"), + (0x0600, 0x06FF, "Arabic"), +) + + +def _substrate_glyph_violations(surface: str) -> list[tuple[int, str, str]]: + """Return ``[(pos, char, block_label), ...]`` for every substrate + glyph in *surface*. Empty list means clean.""" + out: list[tuple[int, str, str]] = [] + for i, ch in enumerate(surface): + cp = ord(ch) + for start, end, label in _FORBIDDEN_BLOCKS: + if start <= cp <= end: + out.append((i, ch, label)) + break + return out + + +@pytest.fixture(scope="module") +def cases(): + return load_cases() + + +@pytest.mark.parametrize("lens_id", _LENS_IDS_TO_TEST) +def test_cognition_lane_surfaces_free_of_substrate_glyphs(cases, lens_id): + report = run_eval(cases, config=RuntimeConfig(anchor_lens_id=lens_id)) + leaks: list[str] = [] + for case in report.cases: + if not case.surface: + continue + violations = _substrate_glyph_violations(case.surface) + if violations: + for pos, ch, block in violations: + leaks.append( + f" case={case.case_id} " + f"substrate_glyph={ch!r} (block={block}) at pos {pos} " + f"surface={case.surface!r}" + ) + assert not leaks, ( + f"anchor_lens_no_glyph_leak violated under lens={lens_id!r}.\n" + "ChatResponse.surface MUST NOT contain substrate-script glyphs " + "(Greek / Hebrew / etc.) regardless of loaded lens " + "(ADR-0073 surface contract). Offending cases:\n" + + "\n".join(leaks) + ) + + +def test_lens_annotation_is_ascii_directly(): + """Independent of the cognition lane: the lens metadata itself + (lens_id, cognitive_mode_label) must be pure ASCII so the + annotation can never carry non-ASCII even if a future composer + forgets to ASCII-check before emit.""" + from packs.anchor_lens import load_anchor_lens + + for lens_id in ("default_unanchored_v1", "grc_logos_v1", "he_logos_v1"): + lens = load_anchor_lens(lens_id) + lens.lens_id.encode("ascii") + lens.cognitive_mode_label.encode("ascii") + for atom in lens.semantic_domain_preferences: + atom.encode("ascii") diff --git a/tests/test_anchor_lens_pack_seam.py b/tests/test_anchor_lens_pack_seam.py index e05be055..1765d661 100644 --- a/tests/test_anchor_lens_pack_seam.py +++ b/tests/test_anchor_lens_pack_seam.py @@ -5,18 +5,14 @@ Pins the load-bearing commitment of ADR-0073 / ADR-0073b: Anchor lens is a composer-side concept, not a property of the proposition graph or trace hash function. -At L1.2 the lens is loaded by ``chat/runtime.py`` and stored on the -runtime, but no other file imports it. The truth-path modules -(cognition / trace / pipeline / intent classification / propagation / -vault / algebra) must NOT import ``packs.anchor_lens``. +At L1.3 the lens is loaded by ``chat/runtime.py`` and consumed by +``chat/pack_grounding.py`` (the composer-side allowlist). The +truth-path modules (cognition / trace / pipeline / intent +classification / propagation / vault / algebra) must NOT import +``packs.anchor_lens``. This test fails the moment anchor lens leaks into the truth path. -L1.3 will widen the allow-list to include ``chat/pack_grounding.py`` -(and any other composer it wires through) at the same time it adds -composer behaviour — exactly the way the register seam was widened at -R2. Truth-path purity remains absolute. - Mirror of ``tests/test_register_pack_seam.py`` for the substantive-axis sibling. """