diff --git a/core/cli.py b/core/cli.py index c7ab0093..56c85cbc 100644 --- a/core/cli.py +++ b/core/cli.py @@ -23,7 +23,7 @@ _CORE_RS_DIR = _REPO_ROOT / "core-rs" _CORE_RS_MANIFEST = _CORE_RS_DIR / "Cargo.toml" DESCRIPTION = "CORE versor engine command suite." -EPILOG = "Examples:\n core chat\n core pulse \"What is truth?\"\n core pulse --no-glove --json \"Compare knowledge and wisdom\"\n core bench\n core bench --suite all\n core bench --suite all --json --report bench_all.json\n core bench --suite determinism --runs 50\n core bench --suite speedup --json\n core trace \"word beginning truth\"\n core trace --output-language grc --frame-pack grc --json \"logos\"\n core rust status\n core rust build\n core oov covenant\n core pack list\n core pack verify en_minimal_v1\n core teaching audit\n core teaching audit --json\n core teaching gaps --top 10\n core teaching queue --threshold 3\n core teaching propose \n core teaching proposals --state pending\n core teaching review --accept --review-date 2026-05-18\n core teaching supersede cause_light_reveals_truth --subject light --intent cause --connective grounds --object truth --review-date 2026-05-18\n core teaching supersessions\n core teaching supersessions --json\n core test --suite fast -q\n core test --suite pulse -q\n core test --suite proof -q\n core test --suite cognition -q\n core test -- tests/test_alignment_graph.py -q\n core demo audit-tour\n core demo register-tour\n core demo anchor-lens-tour\n core demo pack-measurements\n core demo long-context-comparison\n core demo anti-regression\n core demo learning-loop\n core demo articulation\n core demo conversation\n core demo conversation --no-stream\n core demo all\n core demo adr-0024-chain\n core eval --list\n core eval cognition\n core eval cognition --json --save\n core eval cognition --split dev --version v1\n core eval cognition --split holdout" +EPILOG = "Examples:\n core chat\n core pulse \"What is truth?\"\n core pulse --no-glove --json \"Compare knowledge and wisdom\"\n core bench\n core bench --suite all\n core bench --suite all --json --report bench_all.json\n core bench --suite determinism --runs 50\n core bench --suite speedup --json\n core trace \"word beginning truth\"\n core trace --output-language grc --frame-pack grc --json \"logos\"\n core rust status\n core rust build\n core oov covenant\n core pack list\n core pack verify en_minimal_v1\n core teaching audit\n core teaching audit --json\n core teaching gaps --top 10\n core teaching queue --threshold 3\n core teaching propose \n core teaching proposals --state pending\n core teaching review --accept --review-date 2026-05-18\n core teaching supersede cause_light_reveals_truth --subject light --intent cause --connective grounds --object truth --review-date 2026-05-18\n core teaching supersessions\n core teaching supersessions --json\n core test --suite fast -q\n core test --suite pulse -q\n core test --suite proof -q\n core test --suite cognition -q\n core test -- tests/test_alignment_graph.py -q\n core demo audit-tour\n core demo register-tour\n core demo anchor-lens-tour\n core demo orthogonality-tour\n core demo pack-measurements\n core demo long-context-comparison\n core demo anti-regression\n core demo learning-loop\n core demo articulation\n core demo conversation\n core demo conversation --no-stream\n core demo all\n core demo adr-0024-chain\n core eval --list\n core eval cognition\n core eval cognition --json --save\n core eval cognition --split dev --version v1\n core eval cognition --split holdout" _TEST_SUITES: dict[str, tuple[str, ...]] = { "fast": ( @@ -1981,6 +1981,14 @@ def cmd_demo(args: argparse.Namespace) -> int: print(json.dumps(result, indent=2, sort_keys=True, default=str)) return 0 if result.get("all_claims_supported", False) else 1 + if target == "orthogonality-tour": + from evals.orthogonality_tour.run_tour import run_tour as run_ortho_tour + + result = run_ortho_tour(emit_json=args.json) + if args.json: + print(json.dumps(result, indent=2, sort_keys=True, default=str)) + return 0 if result.get("all_claims_supported", False) else 1 + if target == "pack-measurements": from scripts.publish_pack_measurements import ( build_combined_report, @@ -2877,6 +2885,7 @@ def build_parser() -> argparse.ArgumentParser: "audit-tour", "register-tour", "anchor-lens-tour", + "orthogonality-tour", "pack-measurements", "long-context-comparison", "anti-regression", @@ -2901,6 +2910,9 @@ def build_parser() -> argparse.ArgumentParser: "prompts × three lenses; trace_hash DISTINCT across lenses, " "no substrate glyph leak. Opposite invariant from register-tour; " "both must hold continuously. " + "orthogonality-tour: ADR-0074 composition demo — full 3 × 3 × 2 " + "matrix (register × lens × prompts, 18 cells); pins five " + "claims simultaneously including both single-axis invariants. " "pack-measurements: ADR-0043 — pack-layer claims → CI-enforced " "numbers across the three ratified identity packs. " "long-context-comparison: ADR-0045 — CORE exact recall NIAH at " diff --git a/docs/decisions/ADR-0074-orthogonality-tour.md b/docs/decisions/ADR-0074-orthogonality-tour.md new file mode 100644 index 00000000..06e0694d --- /dev/null +++ b/docs/decisions/ADR-0074-orthogonality-tour.md @@ -0,0 +1,181 @@ +# ADR-0074 — Orthogonality tour: anchor-lens × register composition demo + +**Status:** Accepted +**Date:** 2026-05-19 +**Ratified:** 2026-05-19 +**Author:** Shay +**Builds on:** [ADR-0072](./ADR-0072-register-telemetry-operator-surface.md) +(register-tour, R5), [ADR-0073d](./ADR-0073d-anchor-lens-telemetry-tour.md) +(anchor-lens-tour, L1.4) + +--- + +## Context + +Two single-axis tours ship today: + +* `core demo register-tour` — pins `trace_hash CONSTANT` across + registers within a fixed lens. +* `core demo anchor-lens-tour` — pins `trace_hash DISTINCT` across + lenses within a fixed register. + +L1.3's `test_register_seam_within_lens_holds` already parametrizes +the inner claim over every lens choice. ADR-0074 packages **both +claims simultaneously** into a single demo that walks the full +register × lens × prompt matrix and asserts the orthogonality holds +turn-by-turn across the entire grid. + +Without this composition demo, an operator can verify each axis +independently but has no single artifact answering the question: + +> When I drive `core chat --register X --anchor-lens Y`, are the two +> axes actually independent, or does some unintended coupling +> sneak in? + +ADR-0074 ships that artifact as `core demo orthogonality-tour`. + +--- + +## Decision + +### Demo shape + +A single 3 × 3 × 2 grid: + +* **Lenses (3):** `default_unanchored_v1`, `grc_logos_v1`, `he_logos_v1` +* **Registers (3):** `default_neutral_v1`, `terse_v1`, `convivial_v1` +* **Prompts (2):** `"What is knowledge?"` (grc engages), + `"What is truth?"` (he engages) + +That is 18 cells. For each cell the demo records: + +``` +surface, trace_hash, grounding_source, +register_id, register_variant_id, +anchor_lens_id, anchor_lens_mode_label +``` + +### Composed claims + +``` +A) inner_register_invariant_within_lens: + For each (lens, prompt) cell, the three register runs share an + identical trace_hash. + (R5 register-tour, applied 6 times: 3 lenses × 2 prompts.) + +B) outer_lens_distinctness_within_register: + For each (register, prompt) cell where any non-unanchored lens + engages, that engaged lens's trace_hash differs from the + unanchored baseline's trace_hash for the same (register, prompt). + (L1.4 anchor-lens-tour, applied 6 times: 3 registers × 2 prompts.) + +C) surface_carries_register_marker_under_convivial: + For every convivial cell with a non-empty surface, + register_variant_id is non-empty (convivial always picks an + opening from its non-empty bucket). + +D) surface_carries_lens_annotation_when_engaged: + For every cell where a non-unanchored lens engages on the + prompt's en lemma, the surface contains `[lens():]` + and anchor_lens_mode_label is non-empty. + +E) no_substrate_glyph_leak_across_grid: + No cell's surface contains Greek/Hebrew/Syriac/Arabic letter + blocks. (ADR-0073c gate re-asserted across the full matrix.) +``` + +The five claims together pin the orthogonality: A says register +varies the surface without changing the proposition; B says lens +varies the proposition; C and D say the user-visible markers behave +as expected; E says no axis leaks substrate glyphs even under +composition. + +### Files + +``` +evals/orthogonality_tour/__init__.py NEW +evals/orthogonality_tour/run_tour.py NEW + +core/cli.py EDIT + - cmd_demo handler wires orthogonality-tour + - demo target choices add "orthogonality-tour" + - cmd_demo description gains a line for orthogonality-tour + - EPILOG gains "core demo orthogonality-tour" + +tests/test_orthogonality_tour_demo.py NEW + - Five claims pinned individually + - all_claims_supported overall + - grid shape sanity check (3 × 3 × 2 = 18 cells) + +docs/decisions/ADR-0074-orthogonality-tour.md NEW (this file) +``` + +No runtime, composer, loader, pack, or schema changes. The demo is +a pure consumer of the existing telemetry contracts. + +### Invariants pinned + +``` +register-tour (R5) — preserved +anchor-lens-tour (L1.4) — preserved +register_invariant_grounding (R3) — preserved +anchor_lens_byte_identity_null_lift (L1.2) — preserved +anchor_lens_lifts_proposition (L1.3) — preserved +anchor_lens_no_glyph_leak (L1.3) — preserved + +invariant_orthogonality_tour_seam (NEW): + evals/orthogonality_tour/run_tour.py asserts the five composed + claims above on the full 3 × 3 × 2 grid. Exits non-zero on any + violation. Pinned by tests/test_orthogonality_tour_demo.py. +``` + +--- + +## Consequences + +### Capability unlocked + +A single command answers "are register and anchor-lens genuinely +orthogonal under composition?" with structured evidence — 18 cells' +worth of surfaces, trace hashes, and telemetry fields, plus five +falsifiable claim booleans. + +### Cognition lane + +Unchanged. The demo is consumer-only; no production code path is +touched. + +### Performance + +The demo runs 18 chat turns sequentially. At current per-turn +latency this lands in single-digit seconds. No caching changes. + +### Trust boundaries + +* No new mutation surface. Demo only reads telemetry. +* Glyph-leak gate is re-asserted in the demo's own claim bundle so + it can never silently regress under composition. +* CLI choice list is the only operator-visible change; the + underlying flags (`--register`, `--anchor-lens`) are unchanged. + +--- + +## Verification + +``` +python -m pytest tests/test_orthogonality_tour_demo.py -q N passed +core demo orthogonality-tour exit 0 +core demo orthogonality-tour --json stable schema + +Curated lanes (must remain green): + smoke / cognition / teaching / packs / runtime / algebra + +Existing single-axis tours continue to pass: + core demo register-tour exit 0 + core demo anchor-lens-tour exit 0 +``` + +The orthogonality tour's exit code is the canonical composition +gate — if it ever exits non-zero in CI, the orthogonality between +the two axes has regressed and one of the single-axis tours is also +likely to be failing. diff --git a/evals/orthogonality_tour/__init__.py b/evals/orthogonality_tour/__init__.py new file mode 100644 index 00000000..603128f0 --- /dev/null +++ b/evals/orthogonality_tour/__init__.py @@ -0,0 +1,4 @@ +"""Orthogonality tour — composes register × anchor-lens × prompts +into a single 3 × 3 × 2 grid and asserts five orthogonality claims +hold under composition. See ``run_tour.py``. +""" diff --git a/evals/orthogonality_tour/run_tour.py b/evals/orthogonality_tour/run_tour.py new file mode 100644 index 00000000..f1ce5ff3 --- /dev/null +++ b/evals/orthogonality_tour/run_tour.py @@ -0,0 +1,345 @@ +"""Orthogonality tour — composes register × anchor-lens × prompts in +one 3 × 3 × 2 grid (ADR-0074). + +The single-axis tours assert opposite invariants: + + register-tour : per (lens, prompt), trace_hash CONSTANT across + registers (R5 / ADR-0072). + anchor-lens-tour : per (register=neutral, prompt), trace_hash + DISTINCT across engaging lenses + (L1.4 / ADR-0073d). + +This tour packages both claims simultaneously into a single demo +that walks the full register × lens × prompt matrix and asserts +the orthogonality holds turn-by-turn across all 18 cells. + +Composed claims +--------------- + +* **A) inner_register_invariant_within_lens** — for each + (lens, prompt), the three register runs share an identical + trace_hash. +* **B) outer_lens_distinctness_within_register** — for each + (register, prompt) where any non-unanchored lens engages, the + engaged lens's trace_hash differs from the unanchored baseline. +* **C) surface_carries_register_marker_under_convivial** — every + convivial cell with a non-empty surface has a non-empty + ``register_variant_id``. +* **D) surface_carries_lens_annotation_when_engaged** — every + engaged cell carries the ``[lens():]`` annotation and a + non-empty ``anchor_lens_mode_label``. +* **E) no_substrate_glyph_leak_across_grid** — no cell's surface + contains Greek/Hebrew/Syriac/Arabic glyphs. + +Exit code 0 iff every claim holds. +""" + +from __future__ import annotations + +import json +from typing import Any + +from chat.runtime import ChatRuntime +from core.cognition.pipeline import CognitiveTurnPipeline +from core.config import RuntimeConfig + + +_REGISTERS = ( + "default_neutral_v1", + "terse_v1", + "convivial_v1", +) + +_LENSES = ( + "default_unanchored_v1", + "grc_logos_v1", + "he_logos_v1", +) + +_PROMPTS = ( + "What is knowledge?", + "What is truth?", +) + +_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"), +) + + +_VERBOSE = True + + +def _say(*args: Any, **kwargs: Any) -> None: + if _VERBOSE: + print(*args, **kwargs) + + +def _print_header() -> None: + _say() + _say("=" * 76) + _say(" CORE Orthogonality Tour — register × anchor-lens × prompts") + _say("=" * 76) + _say( + " Walks the full 3 × 3 × 2 matrix (18 cells) and asserts:\n" + " A) varying register inside a fixed lens keeps trace_hash CONSTANT\n" + " B) varying lens inside a fixed register moves trace_hash where\n" + " the lens engages (DISTINCT from unanchored baseline)\n" + " C) convivial register attaches discourse markers\n" + " D) engaged lens emits [lens():] annotations\n" + " E) no substrate glyphs (Greek/Hebrew/…) leak into surfaces\n" + "\n" + " Together these pin orthogonality under composition — register and\n" + " anchor-lens never interfere even when both flags are set." + ) + _say() + + +def _substrate_glyph_violations(surface: str) -> list[tuple[int, str, str]]: + 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 + + +def _run_one_cell(register_id: str, lens_id: str, prompt: str) -> dict[str, Any]: + runtime = ChatRuntime(config=RuntimeConfig( + register_pack_id=register_id, + anchor_lens_id=lens_id, + )) + pipeline = CognitiveTurnPipeline(runtime=runtime) + result = pipeline.run(prompt) + turn_event = runtime.turn_log[-1] + return { + "prompt": prompt, + "register_id": register_id, + "lens_id": lens_id, + "surface": turn_event.surface, + "trace_hash": result.trace_hash, + "grounding_source": getattr(turn_event, "grounding_source", ""), + "register_variant_id": getattr(turn_event, "register_variant_id", ""), + "anchor_lens_id": getattr(turn_event, "anchor_lens_id", ""), + "anchor_lens_mode_label": getattr(turn_event, "anchor_lens_mode_label", ""), + } + + +def _build_grid() -> list[dict[str, Any]]: + cells: list[dict[str, Any]] = [] + for register_id in _REGISTERS: + for lens_id in _LENSES: + for prompt in _PROMPTS: + cells.append(_run_one_cell(register_id, lens_id, prompt)) + return cells + + +def _cells_by( + cells: list[dict[str, Any]], + *, + register_id: str | None = None, + lens_id: str | None = None, + prompt: str | None = None, +) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + for c in cells: + if register_id is not None and c["register_id"] != register_id: + continue + if lens_id is not None and c["lens_id"] != lens_id: + continue + if prompt is not None and c["prompt"] != prompt: + continue + out.append(c) + return out + + +def _check_claims(cells: list[dict[str, Any]]) -> dict[str, Any]: + glyph_violations: list[str] = [] + register_invariant_failures: list[str] = [] + lens_distinctness_failures: list[str] = [] + convivial_marker_failures: list[str] = [] + lens_annotation_failures: list[str] = [] + + # Claim E — glyph leak across whole grid. + for c in cells: + for pos, ch, block in _substrate_glyph_violations(c["surface"]): + glyph_violations.append( + f"register={c['register_id']!r} lens={c['lens_id']!r} " + f"prompt={c['prompt']!r}: substrate glyph {ch!r} " + f"(block={block}) at pos {pos}" + ) + + # Claim A — register-tour seam inside every (lens, prompt) cell. + for lens_id in _LENSES: + for prompt in _PROMPTS: + triple = _cells_by(cells, lens_id=lens_id, prompt=prompt) + hashes = {c["trace_hash"] for c in triple} + if len(hashes) != 1: + register_invariant_failures.append( + f"lens={lens_id!r} prompt={prompt!r}: " + f"trace_hashes varied across registers: " + f"{sorted(hashes)}" + ) + + # Claim B — lens distinctness inside every (register, prompt) cell. + # Engaged lenses' trace_hashes must differ from the unanchored + # baseline at the same (register, prompt). + for register_id in _REGISTERS: + for prompt in _PROMPTS: + unanchored_cell = _cells_by( + cells, + register_id=register_id, + lens_id="default_unanchored_v1", + prompt=prompt, + )[0] + for lens_id in _LENSES: + if lens_id == "default_unanchored_v1": + continue + cell = _cells_by( + cells, + register_id=register_id, + lens_id=lens_id, + prompt=prompt, + )[0] + if cell["anchor_lens_mode_label"]: + # Engaged → require trace_hash divergence. + if cell["trace_hash"] == unanchored_cell["trace_hash"]: + lens_distinctness_failures.append( + f"register={register_id!r} lens={lens_id!r} " + f"prompt={prompt!r}: engaged lens did not " + f"move trace_hash (still " + f"{cell['trace_hash'][:12]}...)" + ) + + # Claim C — convivial register attaches discourse markers. + for c in _cells_by(cells, register_id="convivial_v1"): + if c["surface"] and not c["register_variant_id"]: + convivial_marker_failures.append( + f"convivial cell lens={c['lens_id']!r} " + f"prompt={c['prompt']!r} has empty register_variant_id " + f"despite non-empty surface" + ) + + # Claim D — engaged lens emits annotation + non-empty mode_label. + for c in cells: + if c["anchor_lens_mode_label"]: + expected = f"[lens({c['lens_id']}):{c['anchor_lens_mode_label']}]" + if expected not in c["surface"]: + lens_annotation_failures.append( + f"register={c['register_id']!r} lens={c['lens_id']!r} " + f"prompt={c['prompt']!r}: mode_label=" + f"{c['anchor_lens_mode_label']!r} but surface lacks " + f"annotation {expected!r}. Surface: {c['surface']!r}" + ) + + return { + "inner_register_invariant_within_lens": not register_invariant_failures, + "outer_lens_distinctness_within_register": not lens_distinctness_failures, + "surface_carries_register_marker_under_convivial": ( + not convivial_marker_failures + ), + "surface_carries_lens_annotation_when_engaged": not lens_annotation_failures, + "no_substrate_glyph_leak_across_grid": not glyph_violations, + "register_invariant_failures": register_invariant_failures, + "lens_distinctness_failures": lens_distinctness_failures, + "convivial_marker_failures": convivial_marker_failures, + "lens_annotation_failures": lens_annotation_failures, + "glyph_violations": glyph_violations, + } + + +def _print_grid(cells: list[dict[str, Any]]) -> None: + for prompt in _PROMPTS: + _say(f" Prompt: {prompt!r}") + for register_id in _REGISTERS: + _say(f" register = {register_id}") + for lens_id in _LENSES: + cell = _cells_by( + cells, register_id=register_id, lens_id=lens_id, + prompt=prompt, + )[0] + mode = cell["anchor_lens_mode_label"] or "-" + variant = cell["register_variant_id"] or "-" + _say( + f" lens={lens_id:24s} " + f"trace={cell['trace_hash'][:12]}… " + f"mode={mode:18s} variant={variant}" + ) + _say() + + +def run_tour(*, emit_json: bool = False) -> dict[str, Any]: + """Run the orthogonality tour and return a structured report.""" + global _VERBOSE + _VERBOSE = not emit_json + + if not emit_json: + _print_header() + _say(f" Building grid: {len(_REGISTERS)} registers × " + f"{len(_LENSES)} lenses × {len(_PROMPTS)} prompts = " + f"{len(_REGISTERS) * len(_LENSES) * len(_PROMPTS)} cells") + _say() + + cells = _build_grid() + + if not emit_json: + _say("-" * 76) + _say(" Composition grid") + _say("-" * 76) + _print_grid(cells) + + claims = _check_claims(cells) + all_supported = all( + claims[k] for k in ( + "inner_register_invariant_within_lens", + "outer_lens_distinctness_within_register", + "surface_carries_register_marker_under_convivial", + "surface_carries_lens_annotation_when_engaged", + "no_substrate_glyph_leak_across_grid", + ) + ) + + if not emit_json: + _say("=" * 76) + _say(" Orthogonality claims (composition gate)") + _say("=" * 76) + for label, key in ( + ("A) inner_register_invariant_within_lens ", + "inner_register_invariant_within_lens"), + ("B) outer_lens_distinctness_within_register ", + "outer_lens_distinctness_within_register"), + ("C) surface_carries_register_marker_convivial", + "surface_carries_register_marker_under_convivial"), + ("D) surface_carries_lens_annotation_engaged ", + "surface_carries_lens_annotation_when_engaged"), + ("E) no_substrate_glyph_leak_across_grid ", + "no_substrate_glyph_leak_across_grid"), + ): + _say(f" {label}: {claims[key]}") + _say() + _say(f" all_claims_supported : {all_supported}") + _say() + + return { + "registers": list(_REGISTERS), + "lenses": list(_LENSES), + "prompts": list(_PROMPTS), + "cells": cells, + "claims": claims, + "all_claims_supported": all_supported, + } + + +if __name__ == "__main__": # pragma: no cover + import sys + + emit_json = "--json" in sys.argv + report = run_tour(emit_json=emit_json) + if emit_json: + print(json.dumps(report, indent=2, sort_keys=True, default=str)) + sys.exit(0 if report["all_claims_supported"] else 1) diff --git a/tests/test_orthogonality_tour_demo.py b/tests/test_orthogonality_tour_demo.py new file mode 100644 index 00000000..63c61ed3 --- /dev/null +++ b/tests/test_orthogonality_tour_demo.py @@ -0,0 +1,106 @@ +"""Orthogonality tour demo — composition gate (ADR-0074). + +Pins the five composed claims that the single-axis tours only assert +in isolation: + + A) inner_register_invariant_within_lens + B) outer_lens_distinctness_within_register + C) surface_carries_register_marker_under_convivial + D) surface_carries_lens_annotation_when_engaged + E) no_substrate_glyph_leak_across_grid + +Any one failing is the composition-regression signal. +""" + +from __future__ import annotations + +from evals.orthogonality_tour.run_tour import ( + _LENSES, + _PROMPTS, + _REGISTERS, + run_tour, +) + + +def test_tour_returns_structured_report(): + report = run_tour(emit_json=True) + assert set(report) >= { + "registers", "lenses", "prompts", "cells", "claims", + "all_claims_supported", + } + assert list(report["registers"]) == list(_REGISTERS) + assert list(report["lenses"]) == list(_LENSES) + assert list(report["prompts"]) == list(_PROMPTS) + + +def test_grid_has_18_cells(): + """3 registers × 3 lenses × 2 prompts.""" + report = run_tour(emit_json=True) + assert len(report["cells"]) == 18 + + +def test_claim_A_register_invariant_within_each_lens(): + """Inner register-tour: for every (lens, prompt) the three + register runs share an identical trace_hash.""" + report = run_tour(emit_json=True) + assert report["claims"]["inner_register_invariant_within_lens"] is True + assert report["claims"]["register_invariant_failures"] == [] + + +def test_claim_B_lens_distinctness_within_each_register(): + """Inner anchor-lens-tour: engaged lenses diverge from the + unanchored baseline at every (register, prompt) cell.""" + report = run_tour(emit_json=True) + assert report["claims"]["outer_lens_distinctness_within_register"] is True + assert report["claims"]["lens_distinctness_failures"] == [] + + +def test_claim_C_convivial_carries_register_marker(): + report = run_tour(emit_json=True) + assert report["claims"]["surface_carries_register_marker_under_convivial"] is True + assert report["claims"]["convivial_marker_failures"] == [] + + +def test_claim_D_engaged_lens_carries_annotation(): + report = run_tour(emit_json=True) + assert report["claims"]["surface_carries_lens_annotation_when_engaged"] is True + assert report["claims"]["lens_annotation_failures"] == [] + + +def test_claim_E_no_substrate_glyph_leak_across_grid(): + report = run_tour(emit_json=True) + assert report["claims"]["no_substrate_glyph_leak_across_grid"] is True + assert report["claims"]["glyph_violations"] == [] + + +def test_all_claims_supported(): + """Canonical composition gate — every claim must hold or exit non-zero.""" + report = run_tour(emit_json=True) + assert report["all_claims_supported"] is True + + +def test_engaged_cells_appear_for_both_non_trivial_lenses(): + """Sanity: the grid actually exercises engagement. grc_logos_v1 + engages on knowledge; he_logos_v1 engages on truth. Each + non-trivial lens must produce at least one engaged cell + (otherwise the lift claim is vacuously satisfied).""" + report = run_tour(emit_json=True) + grc_engaged = [ + c for c in report["cells"] + if c["lens_id"] == "grc_logos_v1" + and c["anchor_lens_mode_label"] == "systematic" + ] + he_engaged = [ + c for c in report["cells"] + if c["lens_id"] == "he_logos_v1" + and c["anchor_lens_mode_label"] == "covenant-verity" + ] + # grc engages on knowledge in every register (3 cells); same for he on truth. + assert len(grc_engaged) == len(_REGISTERS), ( + f"expected grc_logos_v1 engagement in {len(_REGISTERS)} cells " + f"(once per register on 'What is knowledge?'), got {len(grc_engaged)}" + ) + assert len(he_engaged) == len(_REGISTERS), ( + f"expected he_logos_v1 engagement in {len(_REGISTERS)} cells " + f"(once per register on 'What is truth?'), got {len(he_engaged)}" + )