From 1feec74b1c10657fa9c12aff28af591d645dd930 Mon Sep 17 00:00:00 2001 From: Shay Date: Tue, 19 May 2026 20:21:41 -0700 Subject: [PATCH] =?UTF-8?q?feat(anchor=5Flens):=20ADR-0073d=20=E2=80=94=20?= =?UTF-8?q?L1.4=20telemetry,=20CLI=20flag,=20tour=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L1.4 closes the anchor-lens inside-out arc (L1.1→L1.4 mirroring R1→R5). Substantive axis is now operator-observable, operator-driven, and demo-falsifiable — exactly what R5 did for the register subsystem. Telemetry extension - TurnEvent + ChatResponse gain anchor_lens_id + anchor_lens_mode_label (both default "" → pre-L1.4 byte-identical). - serialize_turn_event surfaces both fields in every JSONL line. - Mode-label extracted via _ANCHOR_LENS_ANNOTATION_RE from the PRE-decoration surface (so register decoration cannot interfere with anchor-lens telemetry). Composer remains the sole source of truth for engagement; the runtime helper is read-only. Operator surface - core chat --anchor-lens CLI flag threads into RuntimeConfig.anchor_lens_id. - Invalid id → AnchorLensError caught at cmd_chat and surfaced as _die("invalid --anchor-lens pack id: ...", code=2) before the REPL launches. - Composes with --register (both flags wire through _runtime_config_from_args). Narrative demo - evals/anchor_lens_tour/run_tour.py walks 2 prompts × 3 ratified lenses ({default_unanchored_v1, grc_logos_v1, he_logos_v1}). Asserts four claims: * lens_ids_recorded_per_turn * trace_hashes_distinct_across_lenses (OPPOSITE of register-tour's identical-hash claim) * surface_propositions_distinct_across_lenses * no_substrate_glyph_leak (block-scoped Greek/Hebrew/ Syriac/Arabic; stylistic punct allowed) - Exit code 0 iff all four hold. - Bundled into `core demo` choices + EPILOG. Tests (30 new) - tests/test_anchor_lens_telemetry.py (16) — TurnEvent shape, serializer keys, runtime emits per lens / per engagement state, ChatResponse mirrors event, mode-label extractor unit. - tests/test_anchor_lens_cli.py (9) — _runtime_config_from_args threading, invalid id fail-fast, parser flag wiring, parser composes with --register. - tests/test_anchor_lens_tour_demo.py (9) — four seam claims pinned individually + all_claims_supported + per-cell anchor_lens_id + unanchored cells empty mode + engaged cells carry mode label. Lane evidence - 30 new L1.4 tests pass. - core demo anchor-lens-tour --json → all_claims_supported: True. - core demo register-tour --json → all_claims_supported: True. Both tours pass simultaneously — orthogonality CI-pinned. - python -m core.cli eval cognition → public 100/100/91.7/100 byte-identical (lens=None / default_unanchored_v1). - Full lane: 2736 passed / 4 skipped / 1 pre-existing failure (+30 over L1.3's 2706; the one failure remains test_all_preamble_explains_combined_run, unrelated). Live demo (canonical proof) P1: 'What is knowledge?' default_unanchored_v1 trace=17c9aabe… mode=(none) grc_logos_v1 trace=0198ad4c… mode=systematic he_logos_v1 trace=17c9aabe… mode=(none) P2: 'What is truth?' default_unanchored_v1 trace=2557f3e8… mode=(none) grc_logos_v1 trace=2557f3e8… mode=(none) he_logos_v1 trace=ec8d84aa… mode=covenant-verity Engagement is substrate-scoped: grc never touches truth, he never touches knowledge. Trace hashes diverge exactly where the lens engages. Trust boundaries - --anchor-lens flag does not bypass ratification; loader still enforces companion mastery report self-seal + ratify-time substrate-atom existence check (ADR-0073b/c). - Mode-label extraction is read-only regex parse; can't forge annotations the composer didn't emit. - Telemetry stays redact-safe — both fields are identifiers / mode labels, not content. include_content=False emits them unconditionally. - No new mutation surface; pack files unchanged. Closes the anchor-lens inside-out arc L1.1 content prerequisite ✓ (ADR-0073a) L1.2 class + loader + unanchored sentinel ✓ (ADR-0073b) L1.3 first lenses + composer wiring ✓ (ADR-0073c) L1.4 telemetry + CLI + tour demo ✓ (this commit) Mirrors the R1→R5 register cadence exactly. Both axes are now operator-observable, CI-falsifiable, audit-traceable, and composable via the orthogonality claim pinned in both tours. --- chat/runtime.py | 61 +++ chat/telemetry.py | 5 + core/cli.py | 34 +- core/physics/identity.py | 11 + .../ADR-0073d-anchor-lens-telemetry-tour.md | 348 ++++++++++++++++++ evals/anchor_lens_tour/__init__.py | 6 + evals/anchor_lens_tour/run_tour.py | 285 ++++++++++++++ tests/test_anchor_lens_cli.py | 99 +++++ tests/test_anchor_lens_telemetry.py | 176 +++++++++ tests/test_anchor_lens_tour_demo.py | 87 +++++ 10 files changed, 1110 insertions(+), 2 deletions(-) create mode 100644 docs/decisions/ADR-0073d-anchor-lens-telemetry-tour.md create mode 100644 evals/anchor_lens_tour/__init__.py create mode 100644 evals/anchor_lens_tour/run_tour.py create mode 100644 tests/test_anchor_lens_cli.py create mode 100644 tests/test_anchor_lens_telemetry.py create mode 100644 tests/test_anchor_lens_tour_demo.py diff --git a/chat/runtime.py b/chat/runtime.py index f1f5e6c7..689091a4 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -78,6 +78,32 @@ from session.correction import CorrectionPass from vault.decompose import default_decomposer, default_gate _TOKEN_RE = re.compile(r"\w+", re.UNICODE) +# ADR-0073d (L1.4) — extracts the engaged ``cognitive_mode_label`` from a +# composer-emitted ``[lens():]`` annotation. The runtime +# uses this read-only to populate the TurnEvent telemetry field; the +# composer remains the only source of truth for engagement. +_ANCHOR_LENS_ANNOTATION_RE = re.compile(r"\[lens\(([^):]+)\):([^\]]+)\]") + + +def _extract_anchor_lens_mode_label(surface: str, lens_id: str) -> str: + """Return the engaged mode_label if *surface* carries a + ``[lens():]`` annotation for the given ``lens_id``. + + Returns ``""`` when: + * surface is empty or contains no lens annotation + * lens_id is empty (no lens loaded) + * the annotation in surface is for a different lens_id (defensive) + + Pure read; no side effects. Telemetry-only — the composer is the + sole source of truth for engagement (ADR-0073c). + """ + if not surface or not lens_id: + return "" + for match in _ANCHOR_LENS_ANNOTATION_RE.finditer(surface): + if match.group(1) == lens_id: + return match.group(2) + return "" + _SEED_ALIASES = { "logos": "\u03bb\u03cc\u03b3\u03bf\u03c2", "dabar": "\u05d3\u05d1\u05e8", @@ -311,6 +337,12 @@ class ChatResponse: # for callers that construct ChatResponse without these fields. register_id: str = "" register_variant_id: str = "" + # ADR-0073d (L1.4) — operator-visible anchor-lens identity per turn. + # Mirrors the TurnEvent fields so callers (CLI, demos, tests) can + # read the lens state from ChatResponse without re-parsing the + # telemetry JSONL. ``""`` defaults preserve pre-L1.4 byte-identity. + anchor_lens_id: str = "" + anchor_lens_mode_label: str = "" class ChatRuntime: @@ -968,6 +1000,18 @@ class ChatRuntime: "" if self.register_pack.is_unregistered() else self.register_pack.register_id ) + # ADR-0073d — anchor-lens telemetry. ``id`` reflects the loaded + # pack (empty for UNANCHORED); ``mode_label`` reflects the + # engaged label this turn (empty when the lens didn't fire on + # this turn's lemma). Mode is extracted from the pre-decoration + # surface so register decoration cannot interfere. + anchor_lens_id_stub = ( + "" if self.anchor_lens.is_unanchored() + else self.anchor_lens.lens_id + ) + anchor_lens_mode_label_stub = _extract_anchor_lens_mode_label( + pre_decoration_surface_stub, anchor_lens_id_stub, + ) verdicts_bundle = TurnVerdicts( identity_score=None, safety_verdict=safety_verdict, @@ -995,6 +1039,8 @@ class ChatRuntime: grounding_source=grounding_source, register_id=register_id_stub, register_variant_id=decoration_stub.variant_id, + anchor_lens_id=anchor_lens_id_stub, + anchor_lens_mode_label=anchor_lens_mode_label_stub, ) self.turn_log.append(stub_event) self._emit_turn_event(stub_event) @@ -1041,6 +1087,8 @@ class ChatRuntime: pre_decoration_surface=pre_decoration_surface_stub, register_id=register_id_stub, register_variant_id=decoration_stub.variant_id, + anchor_lens_id=anchor_lens_id_stub, + anchor_lens_mode_label=anchor_lens_mode_label_stub, ) def chat(self, text: str, max_tokens: int | None = None) -> ChatResponse: @@ -1321,6 +1369,15 @@ class ChatRuntime: "" if self.register_pack.is_unregistered() else self.register_pack.register_id ) + # ADR-0073d — anchor-lens telemetry (main path). See stub-path + # comment above for semantics. + anchor_lens_id_main = ( + "" if self.anchor_lens.is_unanchored() + else self.anchor_lens.lens_id + ) + anchor_lens_mode_label_main = _extract_anchor_lens_mode_label( + pre_decoration_surface_main, anchor_lens_id_main, + ) verdicts_bundle = TurnVerdicts( identity_score=identity_score, safety_verdict=safety_verdict, @@ -1347,6 +1404,8 @@ class ChatRuntime: grounding_source=warm_grounding_source or "vault", register_id=register_id_main, register_variant_id=decoration_main.variant_id, + anchor_lens_id=anchor_lens_id_main, + anchor_lens_mode_label=anchor_lens_mode_label_main, ) self.turn_log.append(turn_event) self._emit_turn_event(turn_event) @@ -1382,6 +1441,8 @@ class ChatRuntime: pre_decoration_surface=pre_decoration_surface_main, register_id=register_id_main, register_variant_id=decoration_main.variant_id, + anchor_lens_id=anchor_lens_id_main, + anchor_lens_mode_label=anchor_lens_mode_label_main, ) def _unknown_domain_response(self, field_state: FieldState, filtered: list[str]) -> ChatResponse: diff --git a/chat/telemetry.py b/chat/telemetry.py index 23df1331..32804b0f 100644 --- a/chat/telemetry.py +++ b/chat/telemetry.py @@ -74,6 +74,11 @@ def serialize_turn_event( # marker buckets, so the wire format degrades cleanly. "register_id": str(getattr(event, "register_id", "") or ""), "register_variant_id": str(getattr(event, "register_variant_id", "") or ""), + # ADR-0073d (L1.4) — operator-visible anchor-lens identity per + # turn. Empty strings on pre-L1.4 events / UNANCHORED runtimes / + # turns where the lens did not engage. + "anchor_lens_id": str(getattr(event, "anchor_lens_id", "") or ""), + "anchor_lens_mode_label": str(getattr(event, "anchor_lens_mode_label", "") or ""), } safety = getattr(event, "safety_verdict", None) if safety is not None: diff --git a/core/cli.py b/core/cli.py index a4af6753..c7ab0093 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 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 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": ( @@ -173,6 +173,7 @@ def _runtime_config_from_args(args: argparse.Namespace): admissibility_threshold=getattr(args, "admissibility_threshold", 0.0), identity_pack=getattr(args, "identity", "") or "", register_pack_id=(getattr(args, "register", None) or None), + anchor_lens_id=(getattr(args, "anchor_lens", None) or None), ) @@ -216,10 +217,13 @@ def cmd_chat(args: argparse.Namespace) -> int: try: runtime = ChatRuntime(config=_runtime_config_from_args(args)) - except Exception as exc: # noqa: BLE001 — surface RegisterPackError / pack-load errors + except Exception as exc: # noqa: BLE001 — surface pack-load errors + from packs.anchor_lens.loader import AnchorLensError from packs.register.loader import RegisterPackError if isinstance(exc, RegisterPackError): _die(f"invalid --register pack id: {exc}", code=2) + if isinstance(exc, AnchorLensError): + _die(f"invalid --anchor-lens pack id: {exc}", code=2) raise show_verdicts = bool(getattr(args, "show_verdicts", False)) while True: @@ -1969,6 +1973,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 == "anchor-lens-tour": + from evals.anchor_lens_tour.run_tour import run_tour as run_lens_tour + + result = run_lens_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, @@ -2521,6 +2533,19 @@ def build_parser() -> argparse.ArgumentParser: "runtime init before the REPL starts." ), ) + chat.add_argument( + "--anchor-lens", + metavar="LENS_ID", + default=None, + dest="anchor_lens", + help=( + "optional anchor-lens pack id (ADR-0073+); default: no " + "lens (unanchored sentinel, byte-identical to " + "default_unanchored_v1). Examples: default_unanchored_v1, " + "grc_logos_v1, he_logos_v1. Invalid ids fail-fast at " + "runtime init before the REPL starts." + ), + ) chat.set_defaults(func=cmd_chat) test = subparsers.add_parser("test", help="run pytest with curated suite aliases or direct passthrough") @@ -2851,6 +2876,7 @@ def build_parser() -> argparse.ArgumentParser: "adr-0024-chain", "audit-tour", "register-tour", + "anchor-lens-tour", "pack-measurements", "long-context-comparison", "anti-regression", @@ -2871,6 +2897,10 @@ def build_parser() -> argparse.ArgumentParser: "register-tour: ADR-0068..0072 presentation-axis seam — same " "prompts × three registers; surface varies, grounding_source " "and trace_hash byte-identical. " + "anchor-lens-tour: ADR-0073 substantive-axis seam — same " + "prompts × three lenses; trace_hash DISTINCT across lenses, " + "no substrate glyph leak. Opposite invariant from register-tour; " + "both must hold continuously. " "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/core/physics/identity.py b/core/physics/identity.py index 5aa0c0e2..57efe939 100644 --- a/core/physics/identity.py +++ b/core/physics/identity.py @@ -301,3 +301,14 @@ class TurnEvent: # byte-identical. register_id: str = "" register_variant_id: str = "" + # ADR-0073d (L1.4) — operator-visible anchor-lens identity per turn. + # ``anchor_lens_id`` is the loaded pack id (e.g. ``"grc_logos_v1"``), + # or ``""`` for the in-memory UNANCHORED sentinel. + # ``anchor_lens_mode_label`` is the engaged ``cognitive_mode_label`` + # when the lens fired on this turn's lemma (extracted from the + # composer-emitted ``[lens():]`` annotation), or ``""`` + # when the lens was loaded but did not engage on this turn's + # lemma, or when no lens was loaded at all. Both default to + # ``""`` so pre-L1.4 callers stay byte-identical. + anchor_lens_id: str = "" + anchor_lens_mode_label: str = "" diff --git a/docs/decisions/ADR-0073d-anchor-lens-telemetry-tour.md b/docs/decisions/ADR-0073d-anchor-lens-telemetry-tour.md new file mode 100644 index 00000000..5c1ef439 --- /dev/null +++ b/docs/decisions/ADR-0073d-anchor-lens-telemetry-tour.md @@ -0,0 +1,348 @@ +# ADR-0073d — Anchor-lens telemetry, CLI, and tour demo (Plan Phase L1.4) + +**Status:** Accepted +**Date:** 2026-05-19 +**Ratified:** 2026-05-19 +**Author:** Shay +**Phase:** Plan Phase L1.4 (operator-visible anchor lens, falsifiable demo) +**Parent:** [ADR-0073](./ADR-0073-anchor-lens-substrate.md) (umbrella) +**Builds on:** [ADR-0073a](./ADR-0073a-anchor-lens-content-phase.md), +[ADR-0073b](./ADR-0073b-anchor-lens-class-loader.md), +[ADR-0073c](./ADR-0073c-anchor-lens-composer-wiring.md) +**Pattern:** mirrors ADR-0072 (register R5 — telemetry + tour demo) + +--- + +## Context + +L1.1–L1.3 built the anchor-lens subsystem inside-out: substrate +content (L1.1), pack class + loader (L1.2), composer wiring with +surface lift on cognition lemmas (L1.3). The lens is loaded, +engages on its intended pack lemmas via the alignment graph, and +appends `[lens():]` to the surface when engaged. + +L1.4 closes the architectural arc by making the lens +**operator-observable, operator-driven, and demo-falsifiable** — +exactly what R5 did for the register subsystem. After L1.4 the +substantive axis is feature-complete enough to compose against the +register axis in operator-visible demos and audit pipelines. + +The load-bearing claim L1.4 ships is the **opposite** of register-tour's: + +``` +register-tour : per prompt, fix lens, vary register → trace_hash CONSTANT +anchor-lens-tour : per prompt, fix register, vary lens → trace_hash DISTINCT +``` + +Both invariants must continue to hold turn-by-turn. L1.4 packages +the second one into a falsifiable demo. + +--- + +## Decision + +L1.4 ships three artifacts: + +1. **Telemetry extension** — `TurnEvent` + `ChatResponse` gain + `anchor_lens_id` (loaded pack id, empty for UNANCHORED) and + `anchor_lens_mode_label` (engaged mode label this turn, empty + when the lens didn't engage on this turn's lemma). +2. **Operator surface** — `core chat --anchor-lens ` CLI flag + wires into `RuntimeConfig.anchor_lens_id`. Invalid id fails + fast at `ChatRuntime.__init__` (mirrors `--register`). +3. **Narrative demo** — `core demo anchor-lens-tour` walks a fixed + 2-prompt sequence under {default_unanchored_v1, grc_logos_v1, + he_logos_v1} and asserts the three load-bearing claims. + +### Telemetry — `TurnEvent` shape extension + +```python +# core/physics/identity.py +@dataclass(frozen=True) +class TurnEvent: + ... + register_id: str = "" + register_variant_id: str = "" + # ADR-0073d (L1.4) — operator-visible anchor-lens identity per turn. + anchor_lens_id: str = "" + anchor_lens_mode_label: str = "" +``` + +* `anchor_lens_id`: the loaded pack's `lens_id`, or `""` for the + in-memory `UNANCHORED` sentinel. Pre-L1.4 callers stay + byte-identical (empty string is the default). +* `anchor_lens_mode_label`: the engaged `cognitive_mode_label` + when the lens fired on this turn's lemma, or `""` when the lens + was loaded but did not engage (different lemma than its + alignment scope), or when no lens was loaded at all. + +Reading this pair tells operators three things at a glance: + +* `(id="", mode="")` — no lens loaded +* `(id="", mode="")` — lens loaded, did not engage on this turn +* `(id="", mode="