feat(anchor_lens): ADR-0073d — L1.4 telemetry, CLI flag, tour demo

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 <id> 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.
This commit is contained in:
Shay 2026-05-19 20:21:41 -07:00
parent b35bec6465
commit 1feec74b1c
10 changed files with 1110 additions and 2 deletions

View file

@ -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(<lens_id>):<mode>]`` 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(<lens_id>):<mode>]`` 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:

View file

@ -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:

View file

@ -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 <candidate-jsonl-path>\n core teaching proposals --state pending\n core teaching review <proposal_id> --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 <candidate-jsonl-path>\n core teaching proposals --state pending\n core teaching review <proposal_id> --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 "

View file

@ -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(<id>):<mode>]`` 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 = ""

View file

@ -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.1L1.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(<id>):<mode>]` 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 <id>` 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="<x>", mode="")` — lens loaded, did not engage on this turn
* `(id="<x>", mode="<label>")` — lens loaded, engaged, surface
carries `[lens(<x>):<label>]`
### Mode-label extraction
The composer (L1.3) embeds the engaged mode label in the surface
string as `[lens(<lens_id>):<mode_label>]`. At telemetry-build
time, the runtime extracts the mode label by reading that
annotation from the pre-decoration surface. A small deterministic
parser:
```python
# chat/runtime.py
_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 an annotation
for ``lens_id``, else ``""``. Pure read; no side effects."""
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 ""
```
This keeps L1.4 a **read-only telemetry pass** over the L1.3
surface. The composer remains the only source of truth for
engagement; the runtime simply mirrors what the composer emitted.
### CLI — `core chat --anchor-lens <id>`
```python
chat.add_argument(
"--anchor-lens",
metavar="LENS_ID",
default=None,
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."
),
)
```
Threads into `_runtime_config_from_args` as
`RuntimeConfig(anchor_lens_id=args.anchor_lens)`. Invalid id ⇒
`AnchorLensError` at `ChatRuntime.__init__` — the CLI catches and
surfaces it as `_die("invalid --anchor-lens pack id: ...", code=2)`
before the REPL launches, exactly as `--register` does.
### Demo — `core demo anchor-lens-tour`
A narrative demo that walks a fixed two-prompt sequence under each
ratified lens, prints a lens × prompt grid, and emits a structured
JSON record with the three load-bearing claims:
```
Lenses exercised:
- default_unanchored_v1 (engagement baseline; no annotation expected)
- grc_logos_v1 (engages on knowledge via ἐπιστήμη)
- he_logos_v1 (engages on truth via אמת)
Prompt sequence (2 prompts; deterministic order):
P1: "What is knowledge?"
P2: "What is truth?"
Per cell, the demo records:
surface, grounding_source, trace_hash, anchor_lens_id, anchor_lens_mode_label.
Load-bearing claims (asserted before exit):
lens_ids_recorded_per_turn : True
trace_hashes_distinct_across_lenses : True (≥ 2 distinct hashes per prompt)
surface_propositions_distinct_across_lenses: True (≥ 2 distinct surfaces per prompt)
no_substrate_glyph_leak : True (surfaces stay ASCII at the lens block)
```
Exit code `0` iff every claim holds. Schema mirrors
`core demo register-tour --json`.
### Files
```
core/physics/identity.py EDIT
- TurnEvent gains anchor_lens_id + anchor_lens_mode_label
chat/runtime.py EDIT
- _extract_anchor_lens_mode_label helper
- both stub + main paths populate the two new fields on
TurnEvent and ChatResponse
- ChatResponse mirrors the two fields
chat/telemetry.py EDIT
- serialize_turn_event surfaces both fields
core/cli.py EDIT
- cmd_chat adds --anchor-lens flag with fail-fast handler
- _runtime_config_from_args threads anchor_lens_id
- demo target choices add "anchor-lens-tour"
- cmd_demo handler wires evals/anchor_lens_tour/run_tour
- EPILOG gains "core demo anchor-lens-tour"
evals/anchor_lens_tour/__init__.py NEW
evals/anchor_lens_tour/run_tour.py NEW
tests/test_anchor_lens_telemetry.py NEW
- TurnEvent default empty / populated under each lens
- serialize_turn_event surfaces both fields
- mode_label is "" when lens loaded but no engagement this turn
- ChatResponse mirrors event fields
tests/test_anchor_lens_cli.py NEW
- _runtime_config_from_args threading
- --anchor-lens parser wiring + default None
- Invalid id ⇒ AnchorLensError at ChatRuntime init
tests/test_anchor_lens_tour_demo.py NEW
- Three seam claims pinned individually
- all_claims_supported overall
- Per-cell anchor_lens_id recorded correctly
- No substrate glyphs in the surfaces
docs/decisions/ADR-0073d-anchor-lens-telemetry-tour.md NEW (this file)
```
### Invariants pinned at L1.4
```
anchor_lens_byte_identity_null_lift (L1.2) — preserved
anchor_lens_lifts_proposition (L1.3) — preserved
anchor_lens_no_glyph_leak (L1.3) — preserved
register-tour seam (R5) — preserved
invariant_anchor_lens_telemetry_visible (NEW):
serialize_turn_event(event) always contains
anchor_lens_id and anchor_lens_mode_label keys; the values
reflect the runtime's loaded lens and the engaged mode label
on this turn (or empty when no engagement).
invariant_anchor_lens_tour_seam (NEW):
evals/anchor_lens_tour/run_tour.py asserts:
- anchor_lens_id recorded per turn (matches runtime config)
- trace_hash DISTINCT across lenses per prompt where lens engages
- surface DISTINCT across lenses per prompt where lens engages
- no substrate-block glyphs in any surface under any lens
Exits non-zero on any violation. Pinned by
tests/test_anchor_lens_tour_demo.py.
```
---
## Consequences
### Capability unlocked at L1.4
* Operators drive any ratified lens from the CLI.
* Every audit JSONL line names which lens was active and whether it
engaged on that turn.
* A single demo proves end-to-end that switching lenses moves
trace_hash and surface — the opposite invariant from
`register-tour`, asserted continuously.
* The anchor-lens × register matrix is now feature-complete enough
to compose: any combination of `--register` × `--anchor-lens` is
operator-driveable and audit-traceable.
### Cognition lane — unchanged
Empty defaults on the new TurnEvent fields preserve byte-identical
output. The L1.2 null-lift and L1.3 lift invariants continue to
hold. The cognition eval public/holdout numbers stay byte-identical
under the unanchored default.
### Backwards compatibility
* `TurnEvent` fields default to `""` — pre-L1.4 callers that
construct `TurnEvent(...)` without the new fields keep working.
* `ChatResponse` defaults likewise.
* Existing telemetry consumers that read JSONL by key access continue
to work; snapshot consumers may need to update — `anchor_lens_id`
and `anchor_lens_mode_label` are added. Snapshot tests document
the new shape.
### Performance
One additional regex match per turn (for `_extract_anchor_lens_
mode_label`) when a lens is loaded. When unanchored, the helper
early-exits on the empty `lens_id`. Negligible.
### Trust boundaries
* **`--anchor-lens` flag does not bypass ratification.** The flag
value is passed through `_find_pack` and the loader's
ratification check (see ADR-0073b). An unratified pack id raises
`AnchorLensError` exactly as a config-driven load would.
* **Mode-label extraction is read-only.** The regex parses a
surface the composer already produced; nothing in L1.4 can
forge a `[lens(...):...]` annotation that the composer didn't
emit, because the regex anchors on the literal
composer-emitted format.
* **Telemetry stays redact-safe.** Neither `anchor_lens_id` nor
`anchor_lens_mode_label` carries surface content; both are pack
identifiers / mode labels. `include_content=False` paths surface
them unconditionally because they're not content.
* **No new mutation surface.** Pack files on disk are not modified
by anything in L1.4.
---
## Verification
```
tests/test_anchor_lens_telemetry.py N passed
tests/test_anchor_lens_cli.py N passed
tests/test_anchor_lens_tour_demo.py N passed
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 anchor-lens-tour exit 0
core demo anchor-lens-tour --json stable schema
core demo register-tour exit 0
(R5 seam still
holds; both
tours coexist)
```
The tour exit code is the canonical L1.4 gate — if `anchor-lens-tour`
ever exits non-zero in CI, the substantive axis has regressed.
---
## Composition with the register axis
`core demo register-tour` and `core demo anchor-lens-tour` test
opposite invariants and both must pass continuously:
```
register-tour : trace_hash CONSTANT across registers
anchor-lens-tour : trace_hash DISTINCT across lenses
```
A future two-axis tour (`anchor-lens × register × prompts`) is
natural follow-on work but deferred — single-axis tours land first,
composition tour after.
---
## Open questions deferred
* **Combined CLI flag composition.** `core chat --register X
--anchor-lens Y` already works at the wiring level (both flags
thread into `RuntimeConfig`). A combined "audit view" demo
showing the orthogonality matrix is a future ADR.
* **TurnVerdicts integration.** Should TurnVerdicts carry
`anchor_lens_id` / `register_id` alongside safety/ethics? Yes
eventually, but L1.4 keeps the fields on TurnEvent itself for now.
A unifying ADR can consolidate later.
* **Mid-session lens switching.** Today the lens is loaded once at
`ChatRuntime.__init__`. A `runtime.set_anchor_lens(<id>)` API
would let an operator switch live. Deferred — needs careful
thinking about replay equivalence across the switch (mirror of
the same deferral for register).

View file

@ -0,0 +1,6 @@
"""Anchor-lens tour — narrative walkthrough demonstrating the
substantive-axis seam (ADR-0073). Same prompts under different
lenses produce structurally different surfaces and trace_hashes
the *opposite* invariant from ``core demo register-tour``. See
``run_tour.py`` for the entry point.
"""

View file

@ -0,0 +1,285 @@
"""Anchor-lens tour — narrative walkthrough demonstrating the
substantive-axis seam (ADR-0073 ADR-0073d).
Walks a fixed two-prompt sequence under three ratified lenses
({default_unanchored_v1, grc_logos_v1, he_logos_v1}) and prints a
grid of per-cell ``(surface, grounding_source, trace_hash,
anchor_lens_id, anchor_lens_mode_label)``.
Load-bearing claims asserted before exit (L1.4 invariants the
*opposite* of register-tour's invariants):
* ``lens_ids_recorded_per_turn`` every TurnEvent records the
loaded lens id (empty for unanchored, lens_id otherwise).
* ``trace_hashes_distinct_across_lenses`` for each prompt where
any lens engages, at least two distinct trace_hashes appear
across the three runs. This is the substantive-axis claim:
switching the lens moves the proposition, not just the surface
text.
* ``surface_propositions_distinct_across_lenses`` at least one
prompt yields at least two distinct surfaces across the lens
triple.
* ``no_substrate_glyph_leak`` no Greek / Hebrew / Syriac / Arabic
letter blocks appear in any cell's surface under any lens
(ADR-0073c hard gate, re-asserted in tour scope).
Exit code 0 iff every claim holds. Designed for ``core demo
anchor-lens-tour`` and the corresponding
``tests/test_anchor_lens_tour_demo.py`` gate.
Composition with register-tour
------------------------------
``register-tour`` asserts trace_hash CONSTANT across registers.
``anchor-lens-tour`` asserts trace_hash DISTINCT across lenses.
Both must hold continuously; failure of either breaks the
orthogonality seam claimed by ADR-0073.
"""
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
_LENSES = (
"default_unanchored_v1",
"grc_logos_v1",
"he_logos_v1",
)
# Prompts are chosen so each non-trivial lens engages on at least one
# of them (grc_logos_v1 on knowledge, he_logos_v1 on truth). The
# unanchored baseline never engages, demonstrating the null-lift floor
# explicitly inside the tour.
_PROMPTS = (
"What is knowledge?",
"What is truth?",
)
#: Forbidden Unicode blocks: substrate letter scripts that anchor lens
#: must not leak (ADR-0073c hard gate).
_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("=" * 72)
_say(" CORE Anchor-lens Tour — substantive-axis seam in three lenses")
_say("=" * 72)
_say(
" Same prompt sequence run under three ratified anchor-lens packs.\n"
" Claim: switching lens MOVES the proposition — trace_hash and\n"
" surface differ across lenses where engagement fires (ADR-0073\n"
" L1.3 lift + L1.4 telemetry). This is the *opposite* invariant\n"
" from `core demo register-tour`, which asserts trace_hash CONSTANT\n"
" across registers. Both invariants must hold continuously."
)
_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_lens(lens_id: str) -> list[dict[str, Any]]:
"""Run the prompt sequence under ``lens_id`` and return per-cell records."""
runtime = ChatRuntime(config=RuntimeConfig(anchor_lens_id=lens_id))
pipeline = CognitiveTurnPipeline(runtime=runtime)
cells: list[dict[str, Any]] = []
for prompt in _PROMPTS:
result = pipeline.run(prompt)
turn_event = runtime.turn_log[-1]
cells.append(
{
"prompt": prompt,
"surface": turn_event.surface,
"grounding_source": getattr(turn_event, "grounding_source", ""),
"trace_hash": result.trace_hash,
"anchor_lens_id": getattr(turn_event, "anchor_lens_id", ""),
"anchor_lens_mode_label": getattr(
turn_event, "anchor_lens_mode_label", ""
),
}
)
return cells
def _print_grid(grid: dict[str, list[dict[str, Any]]]) -> None:
for prompt_idx, prompt in enumerate(_PROMPTS):
_say(f" P{prompt_idx + 1}: {prompt!r}")
for lens_id in _LENSES:
cell = grid[lens_id][prompt_idx]
_say(f" {lens_id:24s} surface = {cell['surface']!r}")
mode = cell["anchor_lens_mode_label"] or "(no engagement)"
_say(
f" {'':24s} trace_hash = {cell['trace_hash'][:12]}"
f"mode_label = {mode}"
)
_say()
def _check_claims(
grid: dict[str, list[dict[str, Any]]],
) -> dict[str, Any]:
"""Return the four load-bearing seam-claim booleans + evidence."""
lens_ids_recorded = True
surfaces_vary_at_least_once = False
trace_hashes_distinct_when_engaged = True
glyph_violations: list[str] = []
per_prompt_evidence: list[dict[str, Any]] = []
for prompt_idx, prompt in enumerate(_PROMPTS):
cells = [grid[lens_id][prompt_idx] for lens_id in _LENSES]
# Telemetry visibility: each cell's anchor_lens_id matches the
# lens we configured (empty when the lens_id was the
# unanchored sentinel — pack id "default_unanchored_v1" still
# records as that string).
for lens_id, cell in zip(_LENSES, cells):
if cell["anchor_lens_id"] != lens_id:
lens_ids_recorded = False
# Glyph-leak gate.
for lens_id, cell in zip(_LENSES, cells):
violations = _substrate_glyph_violations(cell["surface"])
for pos, ch, block in violations:
glyph_violations.append(
f"{prompt!r} × {lens_id}: substrate glyph "
f"{ch!r} (block={block}) at pos {pos}"
)
# Distinctness claims. Any prompt where ≥2 distinct
# surfaces / trace_hashes appear across the lens triple
# counts toward the "vary at least once" gate. For the
# "distinct when engaged" gate, we require that if any lens
# cell has a non-empty mode_label (engaged), its trace_hash
# must differ from the unanchored baseline.
unanchored_cell = cells[0]
surface_set = {c["surface"] for c in cells}
if len(surface_set) > 1:
surfaces_vary_at_least_once = True
for c in cells[1:]:
if c["anchor_lens_mode_label"]:
# Lens engaged; require trace_hash divergence from baseline.
if c["trace_hash"] == unanchored_cell["trace_hash"]:
trace_hashes_distinct_when_engaged = False
per_prompt_evidence.append(
{
"prompt": prompt,
"distinct_surfaces_count": len(surface_set),
"distinct_trace_hashes_count": len(
{c["trace_hash"] for c in cells}
),
"lenses_engaged": [
c["anchor_lens_id"]
for c in cells
if c["anchor_lens_mode_label"]
],
}
)
return {
"lens_ids_recorded_per_turn": lens_ids_recorded,
"trace_hashes_distinct_across_lenses": trace_hashes_distinct_when_engaged,
"surface_propositions_distinct_across_lenses": surfaces_vary_at_least_once,
"no_substrate_glyph_leak": not glyph_violations,
"per_prompt_evidence": per_prompt_evidence,
"glyph_violations": glyph_violations,
}
def run_tour(*, emit_json: bool = False) -> dict[str, Any]:
"""Run the anchor-lens tour end-to-end and return a structured report."""
global _VERBOSE
_VERBOSE = not emit_json
if not emit_json:
_print_header()
grid: dict[str, list[dict[str, Any]]] = {}
for lens_id in _LENSES:
if not emit_json:
_say(f" Running lens: {lens_id}")
grid[lens_id] = _run_one_lens(lens_id)
if not emit_json:
_say()
_say("-" * 72)
_say(" Lens × prompt grid")
_say("-" * 72)
_print_grid(grid)
claims = _check_claims(grid)
all_supported = (
claims["lens_ids_recorded_per_turn"]
and claims["trace_hashes_distinct_across_lenses"]
and claims["surface_propositions_distinct_across_lenses"]
and claims["no_substrate_glyph_leak"]
)
if not emit_json:
_say("=" * 72)
_say(" Load-bearing seam claims (L1.4 invariant)")
_say("=" * 72)
_say(
f" lens_ids_recorded_per_turn : "
f"{claims['lens_ids_recorded_per_turn']}"
)
_say(
f" trace_hashes_distinct_across_lenses : "
f"{claims['trace_hashes_distinct_across_lenses']}"
)
_say(
f" surface_propositions_distinct_across_lenses : "
f"{claims['surface_propositions_distinct_across_lenses']}"
)
_say(
f" no_substrate_glyph_leak : "
f"{claims['no_substrate_glyph_leak']}"
)
_say()
_say(f" all_claims_supported : {all_supported}")
_say()
return {
"lenses": list(_LENSES),
"prompts": list(_PROMPTS),
"grid": grid,
"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)

View file

@ -0,0 +1,99 @@
"""Anchor-lens CLI — ``core chat --anchor-lens <id>`` flag wiring
(ADR-0073d / L1.4).
"""
from __future__ import annotations
import argparse
import pytest
from core.cli import _runtime_config_from_args
from core.config import DEFAULT_CONFIG
def _namespace(**overrides) -> argparse.Namespace:
"""Build a Namespace shaped like cmd_chat's args."""
defaults = dict(
output_language=DEFAULT_CONFIG.output_language,
frame_pack=None,
pack=None,
max_tokens=DEFAULT_CONFIG.max_tokens,
no_cross_language_recall=False,
allow_cross_language_generation=False,
vault_reproject_interval=DEFAULT_CONFIG.vault_reproject_interval,
no_salience=False,
salience_top_k=DEFAULT_CONFIG.salience_top_k,
inhibition_threshold=DEFAULT_CONFIG.inhibition_threshold,
inner_loop_admissibility=False,
admissibility_threshold=0.0,
identity="",
register=None,
anchor_lens=None,
)
defaults.update(overrides)
return argparse.Namespace(**defaults)
def test_runtime_config_default_anchor_lens_is_none():
cfg = _runtime_config_from_args(_namespace())
assert cfg.anchor_lens_id is None
def test_runtime_config_threads_anchor_lens_flag():
cfg = _runtime_config_from_args(_namespace(anchor_lens="grc_logos_v1"))
assert cfg.anchor_lens_id == "grc_logos_v1"
def test_runtime_config_empty_anchor_lens_treated_as_none():
cfg = _runtime_config_from_args(_namespace(anchor_lens=""))
assert cfg.anchor_lens_id is None
def test_runtime_config_anchor_lens_threads_into_runtime():
from chat.runtime import ChatRuntime
cfg = _runtime_config_from_args(_namespace(anchor_lens="grc_logos_v1"))
rt = ChatRuntime(config=cfg)
assert rt.anchor_lens_id == "grc_logos_v1"
assert rt.anchor_lens.lens_id == "grc_logos_v1"
assert not rt.anchor_lens.is_unanchored()
def test_runtime_config_unratified_anchor_lens_id_fails_fast():
from chat.runtime import ChatRuntime
from packs.anchor_lens.loader import AnchorLensError
cfg = _runtime_config_from_args(_namespace(anchor_lens="bogus_v999"))
with pytest.raises(AnchorLensError):
ChatRuntime(config=cfg)
def test_chat_parser_accepts_anchor_lens_flag():
from core.cli import build_parser
parser = build_parser()
ns = parser.parse_args(["chat", "--anchor-lens", "grc_logos_v1"])
assert ns.anchor_lens == "grc_logos_v1"
def test_chat_parser_anchor_lens_defaults_none():
from core.cli import build_parser
parser = build_parser()
ns = parser.parse_args(["chat"])
assert ns.anchor_lens is None
def test_chat_parser_accepts_anchor_lens_and_register_together():
"""Composition of orthogonal axes via the CLI."""
from core.cli import build_parser
parser = build_parser()
ns = parser.parse_args([
"chat",
"--register", "convivial_v1",
"--anchor-lens", "grc_logos_v1",
])
assert ns.register == "convivial_v1"
assert ns.anchor_lens == "grc_logos_v1"

View file

@ -0,0 +1,176 @@
"""Anchor-lens telemetry — TurnEvent + serialize_turn_event carry
anchor_lens_id and anchor_lens_mode_label (ADR-0073d / L1.4).
"""
from __future__ import annotations
import json
from chat.runtime import (
ChatRuntime,
_extract_anchor_lens_mode_label,
)
from chat.telemetry import JsonlBufferSink, serialize_turn_event
from core.config import RuntimeConfig
from core.physics.identity import TurnEvent
def _decode(line: str) -> dict:
return json.loads(line)
# ---------- TurnEvent shape ----------
def test_turn_event_defaults_anchor_lens_fields_empty():
event = TurnEvent(
turn=0,
input_tokens=(),
surface="",
walk_surface="",
articulation_surface="",
dialogue_role="assert",
identity_score=None,
cycle_cost_total=0.0,
vault_hits=0,
versor_condition=0.0,
flagged=False,
)
assert event.anchor_lens_id == ""
assert event.anchor_lens_mode_label == ""
def test_serialize_turn_event_emits_anchor_lens_fields():
event = TurnEvent(
turn=0,
input_tokens=(),
surface="",
walk_surface="",
articulation_surface="",
dialogue_role="assert",
identity_score=None,
cycle_cost_total=0.0,
vault_hits=0,
versor_condition=0.0,
flagged=False,
anchor_lens_id="grc_logos_v1",
anchor_lens_mode_label="systematic",
)
payload = serialize_turn_event(event)
assert payload["anchor_lens_id"] == "grc_logos_v1"
assert payload["anchor_lens_mode_label"] == "systematic"
# ---------- end-to-end runtime ----------
def test_unanchored_runtime_emits_empty_anchor_lens_fields():
runtime = ChatRuntime(config=RuntimeConfig())
sink = JsonlBufferSink()
runtime.attach_telemetry_sink(sink)
runtime.chat("What is knowledge?")
last = _decode(sink.lines[-1])
assert last["anchor_lens_id"] == ""
assert last["anchor_lens_mode_label"] == ""
def test_grc_logos_engaged_turn_emits_mode_label():
runtime = ChatRuntime(
config=RuntimeConfig(anchor_lens_id="grc_logos_v1")
)
sink = JsonlBufferSink()
runtime.attach_telemetry_sink(sink)
runtime.chat("What is knowledge?")
last = _decode(sink.lines[-1])
assert last["anchor_lens_id"] == "grc_logos_v1"
assert last["anchor_lens_mode_label"] == "systematic"
def test_he_logos_engaged_turn_emits_mode_label():
runtime = ChatRuntime(
config=RuntimeConfig(anchor_lens_id="he_logos_v1")
)
sink = JsonlBufferSink()
runtime.attach_telemetry_sink(sink)
runtime.chat("What is truth?")
last = _decode(sink.lines[-1])
assert last["anchor_lens_id"] == "he_logos_v1"
assert last["anchor_lens_mode_label"] == "covenant-verity"
def test_lens_loaded_but_not_engaged_emits_empty_mode_label():
"""grc_logos_v1 doesn't engage on the en lemma 'truth' — lens_id
is recorded but mode_label stays empty."""
runtime = ChatRuntime(
config=RuntimeConfig(anchor_lens_id="grc_logos_v1")
)
sink = JsonlBufferSink()
runtime.attach_telemetry_sink(sink)
runtime.chat("What is truth?")
last = _decode(sink.lines[-1])
assert last["anchor_lens_id"] == "grc_logos_v1"
assert last["anchor_lens_mode_label"] == ""
def test_chat_response_mirrors_event_anchor_lens_fields():
runtime = ChatRuntime(
config=RuntimeConfig(anchor_lens_id="grc_logos_v1")
)
response = runtime.chat("What is knowledge?")
event = runtime.turn_log[-1]
assert response.anchor_lens_id == event.anchor_lens_id == "grc_logos_v1"
assert (
response.anchor_lens_mode_label
== event.anchor_lens_mode_label
== "systematic"
)
def test_default_unanchored_pack_emits_pack_id_and_empty_mode():
runtime = ChatRuntime(
config=RuntimeConfig(anchor_lens_id="default_unanchored_v1")
)
sink = JsonlBufferSink()
runtime.attach_telemetry_sink(sink)
runtime.chat("What is knowledge?")
last = _decode(sink.lines[-1])
assert last["anchor_lens_id"] == "default_unanchored_v1"
assert last["anchor_lens_mode_label"] == ""
# ---------- mode-label extractor unit ----------
def test_extractor_returns_empty_for_empty_surface():
assert _extract_anchor_lens_mode_label("", "grc_logos_v1") == ""
def test_extractor_returns_empty_for_empty_lens_id():
assert _extract_anchor_lens_mode_label(
"X [lens(grc_logos_v1):systematic].", "",
) == ""
def test_extractor_returns_empty_when_no_annotation_present():
assert _extract_anchor_lens_mode_label(
"Plain surface no lens.", "grc_logos_v1",
) == ""
def test_extractor_finds_annotation():
out = _extract_anchor_lens_mode_label(
"X. pack-grounded (Y) [lens(grc_logos_v1):systematic].",
"grc_logos_v1",
)
assert out == "systematic"
def test_extractor_ignores_annotation_for_different_lens_id():
"""Defensive: a surface might carry an annotation for a different
lens (composer bug, future multi-lens compositions). Extractor
only returns mode_label for the requested lens_id."""
out = _extract_anchor_lens_mode_label(
"X. [lens(he_logos_v1):covenant-verity].",
"grc_logos_v1",
)
assert out == ""

View file

@ -0,0 +1,87 @@
"""Anchor-lens tour demo — load-bearing seam claims (ADR-0073d / L1.4).
Pins the four seam claims:
* lens_ids_recorded_per_turn telemetry visibility holds.
* trace_hashes_distinct_across_lenses substantive lift fires.
* surface_propositions_distinct_across_lenses surface lift fires.
* no_substrate_glyph_leak ASCII contract holds.
Any one failing is the L1.4 architectural-regression signal.
"""
from __future__ import annotations
from evals.anchor_lens_tour.run_tour import _LENSES, _PROMPTS, run_tour
def test_tour_returns_structured_report():
report = run_tour(emit_json=True)
assert set(report) >= {
"lenses", "prompts", "grid", "claims", "all_claims_supported",
}
assert list(report["lenses"]) == list(_LENSES)
assert list(report["prompts"]) == list(_PROMPTS)
def test_tour_lens_ids_recorded_per_turn():
report = run_tour(emit_json=True)
assert report["claims"]["lens_ids_recorded_per_turn"] is True
def test_tour_trace_hashes_distinct_across_lenses():
"""L1.3 lift claim, restated as a falsifiable demo invariant — and
the *opposite* of register-tour's trace_hashes_identical claim."""
report = run_tour(emit_json=True)
assert report["claims"]["trace_hashes_distinct_across_lenses"] is True
def test_tour_surface_propositions_distinct_across_lenses():
report = run_tour(emit_json=True)
assert report["claims"]["surface_propositions_distinct_across_lenses"] is True
def test_tour_no_substrate_glyph_leak():
"""ADR-0073c hard gate, re-asserted in tour scope."""
report = run_tour(emit_json=True)
assert report["claims"]["no_substrate_glyph_leak"] is True
assert report["claims"]["glyph_violations"] == []
def test_tour_all_claims_supported():
"""Canonical L1.4 gate — every claim must hold or exit non-zero."""
report = run_tour(emit_json=True)
assert report["all_claims_supported"] is True
def test_tour_grid_carries_anchor_lens_id_per_cell():
"""Each grid cell records the lens that produced it."""
report = run_tour(emit_json=True)
for lens_id in _LENSES:
cells = report["grid"][lens_id]
assert len(cells) == len(_PROMPTS)
for cell in cells:
assert cell["anchor_lens_id"] == lens_id
def test_tour_unanchored_cells_have_empty_mode_label():
"""The unanchored baseline never engages, so its mode_label is
always empty regardless of prompt."""
report = run_tour(emit_json=True)
for cell in report["grid"]["default_unanchored_v1"]:
assert cell["anchor_lens_mode_label"] == ""
def test_tour_engaged_cells_carry_mode_label():
"""grc_logos_v1 engages on knowledge; he_logos_v1 engages on truth."""
report = run_tour(emit_json=True)
grc_knowledge = next(
c for c in report["grid"]["grc_logos_v1"]
if c["prompt"] == "What is knowledge?"
)
assert grc_knowledge["anchor_lens_mode_label"] == "systematic"
he_truth = next(
c for c in report["grid"]["he_logos_v1"]
if c["prompt"] == "What is truth?"
)
assert he_truth["anchor_lens_mode_label"] == "covenant-verity"