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="