feat(register): R5 — operator-visible register telemetry + tour demo

ADR-0072 ratified + implemented.  Closes the register subsystem
inside-out arc (R1 ADR-0068 → R5 ADR-0072): the presentation axis is
now operator-visible, CI-falsifiable, and audit-traceable.

Telemetry extension
  - TurnEvent + ChatResponse gain register_id + register_variant_id
    (12-char SHA-256 prefix of selected (opening, closing) pair;
    empty string for UNREGISTERED / no-decoration registers).
  - serialize_turn_event surfaces both fields in every audit JSONL
    line.  Pre-R5 callers stay byte-identical (defaults are "").

Decoration result widened
  - chat/register_variation.py: decorate_surface now returns
    DecorationResult(surface, opening, closing, variant_id).
  - decorate_surface_str alias preserves the pre-R5 string-only API
    for off-runtime callers.
  - chat/runtime.py updated at both call sites (stub + main).

Operator surface
  - core chat --register REGISTER_ID threads into
    RuntimeConfig.register_pack_id via _runtime_config_from_args.
  - Invalid id ⇒ RegisterPackError caught at cmd_chat and surfaced
    as a clean _die(...) before the REPL launches.

Narrative demo
  - evals/register_tour/run_tour.py walks 4 prompts × 3 ratified
    registers ({default_neutral_v1, terse_v1, convivial_v1}) and
    asserts three load-bearing seam claims:
      * all_grounding_sources_identical
      * all_trace_hashes_identical (ADR-0069 invariant C, falsifiable)
      * surfaces_vary_at_least_once (ADR-0071 seeded variation lift)
  - core demo register-tour exit code = 0 iff every claim holds.

Tests
  - tests/test_register_telemetry.py (6) — TurnEvent default,
    serializer keys, runtime emits register_id/variant_id for
    convivial/terse/unregistered, ChatResponse mirrors event fields.
  - tests/test_register_cli.py (7) — _runtime_config_from_args
    threading, invalid-id fail-fast, parser wires --register.
  - tests/test_register_tour_demo.py (7) — three seam claims pinned
    individually + all_claims_supported + per-cell register_id +
    variant_id discipline (empty for neutral/terse, non-empty for
    convivial).
  - tests/test_register_variation.py extended (4 new) — DecorationResult
    shape, decorate_surface_str alias, variant_id stability,
    bijection between non-trivial marker pairs and variant_ids.

Lane evidence
  - Full lane: 2632 passed / 4 skipped / 1 pre-existing failure
    (tests/test_cli_demo.py::test_all_preamble_explains_combined_run,
    unrelated to R5).
  - Cognition eval byte-identical: public 100 / 100 / 91.7 / 100.

Trust boundaries (per CLAUDE.md)
  - --register flag does not bypass ratification; loader validates the
    pack id through _find_pack and the ratify gate at load time.
  - variant_id is content-addressed; no raw markers leak into audit.
  - Telemetry stays redact-safe — register_id and variant_id are
    identifiers, not content, so include_content=False emits them
    unconditionally.
  - No new mutation surface; pack files on disk are not modified.
This commit is contained in:
Shay 2026-05-19 19:03:07 -07:00
parent 6207b5fd0e
commit 7f0bad3e20
12 changed files with 744 additions and 23 deletions

View file

@ -1,4 +1,4 @@
"""Seeded surface variation (ADR-0071, Plan Phase R4).
"""Seeded surface variation (ADR-0071 R4, extended ADR-0072 R5).
Deterministic discourse-marker selection from bounded register-pack
buckets, keyed on ``(seed_text, register_id, turn_idx, bucket_name)``.
@ -28,15 +28,44 @@ imports from ``packs.register`` and from this module.
The selector is uniform-mod-len: every entry in a bucket is equally
likely across the seed space. Frequency shaping (weighted entries) is
deferred per ADR-0071 §Open questions.
ADR-0072 (R5) :func:`decorate_surface` now returns a
:class:`DecorationResult` that carries the post-decoration surface plus
the chosen ``opening`` / ``closing`` strings and a 12-char
``variant_id`` digest of the selected pair. This is what
``TurnEvent`` records into the audit stream so operators can ask
"which register variant fired on turn N?" without reading content.
The legacy string-return is preserved via :func:`decorate_surface_str`.
"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass
from packs.register.loader import RegisterPack
_VARIANT_ID_LEN = 12
@dataclass(frozen=True, slots=True)
class DecorationResult:
"""Result of one seeded discourse-marker decoration.
``variant_id`` is the 12-char SHA-256 prefix of
``f"{opening}|{closing}"``. Empty string no decoration was
applied this turn (empty buckets, or empty input surface). Two
different turns under the same register that select the same
``(opening, closing)`` pair share the same ``variant_id``.
"""
surface: str
opening: str
closing: str
variant_id: str
def _select_bucket_entry(
bucket: tuple[str, ...],
*,
@ -61,20 +90,38 @@ def _select_bucket_entry(
return bucket[idx]
def _compute_variant_id(opening: str, closing: str) -> str:
"""12-char SHA-256 prefix of the chosen ``(opening, closing)`` pair.
Empty when both markers are empty ``""`` is the "no decoration
applied" sentinel, so ``UNREGISTERED`` / ``default_neutral_v1`` /
``terse_v1`` do not pollute the audit stream with a no-op digest.
"""
if not opening and not closing:
return ""
payload = f"{opening}|{closing}".encode("utf-8")
return hashlib.sha256(payload).hexdigest()[:_VARIANT_ID_LEN]
def decorate_surface(
surface: str,
register: RegisterPack,
*,
turn_idx: int,
seed_text: str | None = None,
) -> str:
) -> DecorationResult:
"""Apply seeded discourse-marker decoration to *surface*.
Empty buckets no-op (the original surface is returned). Order
is ``"{opening} {surface}{closing}"`` closing concatenates
directly so an entry like ``" — make sense?"`` carries its own
spacing. Empty-string entries in a bucket count as legitimate
selections (the seed may pick "no marker this turn").
Returns a :class:`DecorationResult` with the post-decoration
``surface`` plus the chosen ``opening`` / ``closing`` markers and
the 12-char ``variant_id`` digest of that pair.
Empty buckets no-op (the original surface is returned, both
marker strings empty, ``variant_id=""``). Order is
``"{opening} {surface}{closing}"`` closing concatenates directly
so an entry like ``" — make sense?"`` carries its own spacing.
Empty-string entries in a bucket count as legitimate selections
(the seed may pick "no marker this turn").
``seed_text`` defaults to *surface* the pre-decoration string is
the natural seed. Callers with a stronger deterministic key (e.g.
@ -84,7 +131,9 @@ def decorate_surface(
they sit until a later phase that owns clause-boundary detection.
"""
if not surface:
return surface
return DecorationResult(
surface=surface, opening="", closing="", variant_id=""
)
if seed_text is None:
seed_text = surface
markers = register.discourse_markers
@ -107,7 +156,29 @@ def decorate_surface(
out = f"{opening} {out}"
if closing:
out = f"{out}{closing}"
return out
return DecorationResult(
surface=out,
opening=opening,
closing=closing,
variant_id=_compute_variant_id(opening, closing),
)
__all__ = ("decorate_surface",)
def decorate_surface_str(
surface: str,
register: RegisterPack,
*,
turn_idx: int,
seed_text: str | None = None,
) -> str:
"""String-only convenience wrapper around :func:`decorate_surface`.
Preserves the pre-R5 return type for off-runtime callers (tests,
ad-hoc CLI tools) that only want the post-decoration string.
"""
return decorate_surface(
surface, register, turn_idx=turn_idx, seed_text=seed_text
).surface
__all__ = ("DecorationResult", "decorate_surface", "decorate_surface_str")

View file

@ -303,6 +303,13 @@ class ChatResponse:
# the truth path (ADR-0069 invariant C). Empty string ⇒ identical
# to ``surface`` (no decoration applied this turn).
pre_decoration_surface: str = ""
# ADR-0072 (R5) — operator-visible register identity per turn.
# Mirrors the TurnEvent fields so callers (CLI, demos, tests) can
# read the register state from ChatResponse without re-parsing the
# telemetry JSONL. ``""`` defaults preserve pre-R5 byte-identity
# for callers that construct ChatResponse without these fields.
register_id: str = ""
register_variant_id: str = ""
class ChatRuntime:
@ -933,11 +940,16 @@ class ChatRuntime:
# the truth-path surface and trace_hash stays invariant under
# register (ADR-0069 invariant C).
pre_decoration_surface_stub = response_surface
response_surface = decorate_surface(
decoration_stub = decorate_surface(
response_surface,
self.register_pack,
turn_idx=len(self.turn_log),
)
response_surface = decoration_stub.surface
register_id_stub = (
"" if self.register_pack.is_unregistered()
else self.register_pack.register_id
)
verdicts_bundle = TurnVerdicts(
identity_score=None,
safety_verdict=safety_verdict,
@ -963,6 +975,8 @@ class ChatRuntime:
ethics_verdict=ethics_verdict,
verdicts=verdicts_bundle,
grounding_source=grounding_source,
register_id=register_id_stub,
register_variant_id=decoration_stub.variant_id,
)
self.turn_log.append(stub_event)
self._emit_turn_event(stub_event)
@ -1007,6 +1021,8 @@ class ChatRuntime:
verdicts=verdicts_bundle,
grounding_source=grounding_source,
pre_decoration_surface=pre_decoration_surface_stub,
register_id=register_id_stub,
register_variant_id=decoration_stub.variant_id,
)
def chat(self, text: str, max_tokens: int | None = None) -> ChatResponse:
@ -1277,11 +1293,16 @@ class ChatRuntime:
# cognition pipeline can hash the truth-path surface and
# trace_hash stays invariant under register (ADR-0069 inv C).
pre_decoration_surface_main = response_surface
response_surface = decorate_surface(
decoration_main = decorate_surface(
response_surface,
self.register_pack,
turn_idx=len(self.turn_log),
)
response_surface = decoration_main.surface
register_id_main = (
"" if self.register_pack.is_unregistered()
else self.register_pack.register_id
)
verdicts_bundle = TurnVerdicts(
identity_score=identity_score,
safety_verdict=safety_verdict,
@ -1306,6 +1327,8 @@ class ChatRuntime:
ethics_verdict=ethics_verdict,
verdicts=verdicts_bundle,
grounding_source=warm_grounding_source or "vault",
register_id=register_id_main,
register_variant_id=decoration_main.variant_id,
)
self.turn_log.append(turn_event)
self._emit_turn_event(turn_event)
@ -1339,6 +1362,8 @@ class ChatRuntime:
verdicts=verdicts_bundle,
grounding_source=warm_grounding_source or "vault",
pre_decoration_surface=pre_decoration_surface_main,
register_id=register_id_main,
register_variant_id=decoration_main.variant_id,
)
def _unknown_domain_response(self, field_state: FieldState, filtered: list[str]) -> ChatResponse:

View file

@ -69,6 +69,11 @@ def serialize_turn_event(
"flagged": bool(getattr(event, "flagged", False)),
"stub_path": getattr(event, "walk_surface", "") == _UNKNOWN_DOMAIN_SURFACE,
"dialogue_role": str(getattr(event, "dialogue_role", "")),
# ADR-0072 (R5) — operator-visible register identity per turn.
# Empty strings on pre-R5 events / UNREGISTERED runtimes / empty
# 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 ""),
}
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 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 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": (
@ -172,6 +172,7 @@ def _runtime_config_from_args(args: argparse.Namespace):
inner_loop_admissibility=getattr(args, "inner_loop_admissibility", False),
admissibility_threshold=getattr(args, "admissibility_threshold", 0.0),
identity_pack=getattr(args, "identity", "") or "",
register_pack_id=(getattr(args, "register", None) or None),
)
@ -213,7 +214,13 @@ def cmd_chat(args: argparse.Namespace) -> int:
except Exception as exc: # pragma: no cover - exercised by CLI in broken envs
_print_runtime_import_hint(exc)
runtime = ChatRuntime(config=_runtime_config_from_args(args))
try:
runtime = ChatRuntime(config=_runtime_config_from_args(args))
except Exception as exc: # noqa: BLE001 — surface RegisterPackError / pack-load errors
from packs.register.loader import RegisterPackError
if isinstance(exc, RegisterPackError):
_die(f"invalid --register pack id: {exc}", code=2)
raise
show_verdicts = bool(getattr(args, "show_verdicts", False))
while True:
try:
@ -1954,6 +1961,14 @@ def cmd_demo(args: argparse.Namespace) -> int:
print(json.dumps(result, indent=2, sort_keys=True, default=str))
return 0
if target == "register-tour":
from evals.register_tour.run_tour import run_tour as run_register_tour
result = run_register_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,
@ -2494,6 +2509,18 @@ def build_parser() -> argparse.ArgumentParser:
"stderr (ADR-0041 operator-facing audit readout)"
),
)
chat.add_argument(
"--register",
metavar="REGISTER_ID",
default=None,
help=(
"optional register pack id (ADR-0068+); default: no "
"register (unregistered sentinel, byte-identical to "
"default_neutral_v1). Examples: default_neutral_v1, "
"terse_v1, convivial_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")
@ -2823,6 +2850,7 @@ def build_parser() -> argparse.ArgumentParser:
"phase6",
"adr-0024-chain",
"audit-tour",
"register-tour",
"pack-measurements",
"long-context-comparison",
"anti-regression",
@ -2840,6 +2868,9 @@ def build_parser() -> argparse.ArgumentParser:
"consolidated PASS/FAIL table; exits non-zero if any demo fails. "
"audit-tour: ADR-0027..0041 pack-layer architecture in four "
"scenes (identity / safety / ethics / replay). "
"register-tour: ADR-0068..0072 presentation-axis seam — same "
"prompts × three registers; surface varies, grounding_source "
"and trace_hash byte-identical. "
"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

@ -291,3 +291,13 @@ class TurnEvent:
# Preserved verbatim through the TurnEvent telemetry stream for
# downstream audit consumers.
grounding_source: str = "none"
# ADR-0072 (R5) — operator-visible register identity per turn.
# ``register_id`` is the loaded pack id (e.g. ``"convivial_v1"``),
# or ``""`` for the in-memory UNREGISTERED sentinel.
# ``register_variant_id`` is the 12-char SHA-256 prefix of the
# selected ``(opening, closing)`` discourse-marker pair, or ``""``
# when no decoration was applied this turn (empty buckets, or empty
# surface). Both default to ``""`` so pre-R5 callers stay
# byte-identical.
register_id: str = ""
register_variant_id: str = ""

View file

@ -1,7 +1,8 @@
# ADR-0072 — Register telemetry + operator surface (Plan Phase R5)
**Status:** Proposed
**Status:** Accepted
**Date:** 2026-05-19
**Ratified:** 2026-05-19
**Author:** Shay
**Phase:** Plan Phase R5 (operator-visible register)
**Builds on:** ADR-0068 (register pack class), ADR-0069 (realizer

View file

@ -0,0 +1,5 @@
"""Register tour — narrative walkthrough demonstrating CORE's
presentation-axis seam: switching register packs varies the surface
string while leaving ``grounding_source`` and ``trace_hash``
byte-identical. See ``run_tour.py`` for the entry point.
"""

View file

@ -0,0 +1,227 @@
"""Register tour — narrative walkthrough demonstrating the
presentation-axis seam (ADR-0068 ADR-0072).
Walks a fixed four-prompt sequence under three ratified registers
({default_neutral_v1, terse_v1, convivial_v1}) and prints a grid of
per-cell ``(surface, grounding_source, trace_hash, register_id,
register_variant_id)``.
Load-bearing claim asserted before exit (R5 invariant):
* ``all_grounding_sources_identical`` for every prompt, the
grounding_source is byte-identical across registers.
* ``all_trace_hashes_identical`` for every prompt, the trace_hash is
byte-identical across registers.
* ``surfaces_vary_at_least_once`` at least one prompt produces a
visibly different surface under convivial vs neutral (the variation
is real, not stubbed).
Exit code 0 iff every claim holds. Designed for ``core demo
register-tour`` and the corresponding ``tests/test_register_tour_demo.py``
gate.
"""
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
# Three ratified registers cover the three structural points of the
# R1R4 architecture: unregistered-equivalent neutral baseline, the
# realizer-override variant (terse_v1), and the seeded-variation
# variant (convivial_v1).
_REGISTERS = (
"default_neutral_v1",
"terse_v1",
"convivial_v1",
)
# A fixed, deterministic prompt sequence. Each prompt is chosen to
# exercise a different grounding source on the cold path so the
# register-invariance claim holds across the breadth of the cognition
# lane, not just one intent shape.
_PROMPTS = (
"What is light?",
"Define knowledge.",
"What is truth?",
"Light reveals truth, right?",
)
_VERBOSE = True
def _say(*args: Any, **kwargs: Any) -> None:
if _VERBOSE:
print(*args, **kwargs)
def _print_header() -> None:
_say()
_say("=" * 72)
_say(" CORE Register Tour — presentation-axis seam in three registers")
_say("=" * 72)
_say(
" Same prompt sequence run under three ratified register packs.\n"
" Claim: switching register varies SURFACE only — grounding_source\n"
" and trace_hash stay byte-identical (ADR-0069 invariant C,\n"
" ADR-0070 register_invariant_grounding, ADR-0071 seeded variation\n"
" replay equivalence, ADR-0072 operator-visible audit)."
)
_say()
def _run_one_register(register_id: str) -> list[dict[str, Any]]:
"""Run the prompt sequence under ``register_id`` and return per-cell records."""
runtime = ChatRuntime(config=RuntimeConfig(register_pack_id=register_id))
pipeline = CognitiveTurnPipeline(runtime=runtime)
cells: list[dict[str, Any]] = []
for prompt in _PROMPTS:
# ``pipeline.run(prompt)`` computes trace_hash on the pre-
# decoration surface (ADR-0069 invariant C). We want the
# *post-decoration* surface (the user-facing string) for the
# surface-varies claim, which lives on TurnEvent.surface after
# ChatRuntime applied seeded variation.
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,
"register_id": getattr(turn_event, "register_id", ""),
"register_variant_id": getattr(turn_event, "register_variant_id", ""),
}
)
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 register_id in _REGISTERS:
cell = grid[register_id][prompt_idx]
_say(
f" {register_id:24s} surface = {cell['surface']!r}"
)
_say(
f" {'':24s} grounding = {cell['grounding_source']:<10s}"
f" trace_hash = {cell['trace_hash'][:12]}"
f" variant_id = {cell['register_variant_id'] or '(none)'}"
)
_say()
def _check_claims(
grid: dict[str, list[dict[str, Any]]],
) -> dict[str, Any]:
"""Return the three load-bearing seam-claim booleans + supporting evidence."""
all_grounding_identical = True
all_trace_hashes_identical = True
surfaces_vary_at_least_once = False
per_prompt_evidence: list[dict[str, Any]] = []
for prompt_idx, prompt in enumerate(_PROMPTS):
cells = [grid[r][prompt_idx] for r in _REGISTERS]
grounding_set = {c["grounding_source"] for c in cells}
trace_set = {c["trace_hash"] for c in cells}
surface_set = {c["surface"] for c in cells}
if len(grounding_set) > 1:
all_grounding_identical = False
if len(trace_set) > 1:
all_trace_hashes_identical = False
if len(surface_set) > 1:
surfaces_vary_at_least_once = True
per_prompt_evidence.append(
{
"prompt": prompt,
"distinct_grounding_sources": sorted(grounding_set),
"distinct_trace_hashes": sorted(trace_set),
"distinct_surfaces_count": len(surface_set),
}
)
return {
"all_grounding_sources_identical": all_grounding_identical,
"all_trace_hashes_identical": all_trace_hashes_identical,
"surfaces_vary_at_least_once": surfaces_vary_at_least_once,
"per_prompt_evidence": per_prompt_evidence,
}
def run_tour(*, emit_json: bool = False) -> dict[str, Any]:
"""Run the register tour end-to-end and return a structured report.
When ``emit_json`` is True the human narration is suppressed and
only the result dict is returned (caller prints it). Otherwise
narration is printed and the dict is returned for index callers.
"""
global _VERBOSE
_VERBOSE = not emit_json
if not emit_json:
_print_header()
grid: dict[str, list[dict[str, Any]]] = {}
for register_id in _REGISTERS:
if not emit_json:
_say(f" Running register: {register_id}")
grid[register_id] = _run_one_register(register_id)
if not emit_json:
_say()
_say("-" * 72)
_say(" Register × prompt grid")
_say("-" * 72)
_print_grid(grid)
claims = _check_claims(grid)
all_supported = (
claims["all_grounding_sources_identical"]
and claims["all_trace_hashes_identical"]
and claims["surfaces_vary_at_least_once"]
)
if not emit_json:
_say("=" * 72)
_say(" Load-bearing seam claims (R5 invariant)")
_say("=" * 72)
_say(
f" all_grounding_sources_identical : "
f"{claims['all_grounding_sources_identical']}"
)
_say(
f" all_trace_hashes_identical : "
f"{claims['all_trace_hashes_identical']}"
)
_say(
f" surfaces_vary_at_least_once : "
f"{claims['surfaces_vary_at_least_once']}"
)
_say()
_say(f" all_claims_supported : {all_supported}")
_say()
return {
"registers": list(_REGISTERS),
"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,93 @@
"""Register CLI — ``core chat --register <id>`` flag wiring
(ADR-0072 / Plan Phase R5).
"""
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, allowing
targeted overrides for register / identity."""
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,
)
defaults.update(overrides)
return argparse.Namespace(**defaults)
def test_runtime_config_default_register_is_none():
"""No --register on CLI ⇒ register_pack_id is None ⇒ unregistered."""
cfg = _runtime_config_from_args(_namespace())
assert cfg.register_pack_id is None
def test_runtime_config_threads_register_flag():
"""--register convivial_v1 ⇒ RuntimeConfig.register_pack_id set."""
cfg = _runtime_config_from_args(_namespace(register="convivial_v1"))
assert cfg.register_pack_id == "convivial_v1"
def test_runtime_config_empty_register_treated_as_none():
"""Empty-string --register (treated as unset) ⇒ None."""
cfg = _runtime_config_from_args(_namespace(register=""))
assert cfg.register_pack_id is None
def test_runtime_config_register_pack_id_threads_into_runtime():
"""Loading the runtime with a ratified register id wires
ChatRuntime.register_pack to a non-unregistered pack."""
from chat.runtime import ChatRuntime
cfg = _runtime_config_from_args(_namespace(register="convivial_v1"))
rt = ChatRuntime(config=cfg)
assert rt.register_pack_id == "convivial_v1"
assert rt.register_pack.register_id == "convivial_v1"
assert not rt.register_pack.is_unregistered()
def test_runtime_config_unratified_register_id_fails_fast():
"""An unratified / non-existent register id ⇒ RegisterPackError at
ChatRuntime init, not at first turn."""
from chat.runtime import ChatRuntime
from packs.register.loader import RegisterPackError
cfg = _runtime_config_from_args(_namespace(register="bogus_v999"))
with pytest.raises(RegisterPackError):
ChatRuntime(config=cfg)
def test_chat_parser_accepts_register_flag():
"""``core chat --register convivial_v1`` parses cleanly."""
from core.cli import build_parser
parser = build_parser()
ns = parser.parse_args(["chat", "--register", "convivial_v1"])
assert ns.register == "convivial_v1"
def test_chat_parser_register_defaults_none():
from core.cli import build_parser
parser = build_parser()
ns = parser.parse_args(["chat"])
assert ns.register is None

View file

@ -0,0 +1,113 @@
"""Register telemetry — TurnEvent + serialize_turn_event carry
register_id and register_variant_id (ADR-0072 / Plan Phase R5).
"""
from __future__ import annotations
import json
from chat.runtime import ChatRuntime
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)
def test_turn_event_defaults_register_fields_empty():
"""Pre-R5 callers constructing TurnEvent without the new fields
see empty strings preserves byte-identity."""
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.register_id == ""
assert event.register_variant_id == ""
def test_serialize_turn_event_emits_register_fields():
"""The JSONL audit line carries register_id / register_variant_id."""
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,
register_id="convivial_v1",
register_variant_id="abcdef012345",
)
payload = serialize_turn_event(event)
assert payload["register_id"] == "convivial_v1"
assert payload["register_variant_id"] == "abcdef012345"
def test_runtime_chat_emits_register_id_when_set():
"""Convivial-loaded runtime ⇒ every TurnEvent carries register_id."""
runtime = ChatRuntime(
config=RuntimeConfig(register_pack_id="convivial_v1")
)
sink = JsonlBufferSink()
runtime.attach_telemetry_sink(sink)
runtime.chat("What is light?")
assert sink.lines, "expected at least one telemetry line"
last = _decode(sink.lines[-1])
assert last["register_id"] == "convivial_v1"
# convivial_v1 always emits a non-empty opening on non-empty
# surfaces, so variant_id is a 12-char hex.
variant_id = last["register_variant_id"]
assert variant_id != ""
assert len(variant_id) == 12
int(variant_id, 16)
def test_runtime_chat_empty_register_id_for_unregistered():
"""Default runtime (no --register) ⇒ empty register_id / variant_id."""
runtime = ChatRuntime(config=RuntimeConfig())
sink = JsonlBufferSink()
runtime.attach_telemetry_sink(sink)
runtime.chat("What is light?")
last = _decode(sink.lines[-1])
assert last["register_id"] == ""
assert last["register_variant_id"] == ""
def test_runtime_chat_empty_variant_id_for_terse():
"""terse_v1 has empty marker buckets ⇒ variant_id stays empty even
though register_id is set."""
runtime = ChatRuntime(
config=RuntimeConfig(register_pack_id="terse_v1")
)
sink = JsonlBufferSink()
runtime.attach_telemetry_sink(sink)
runtime.chat("What is light?")
last = _decode(sink.lines[-1])
assert last["register_id"] == "terse_v1"
assert last["register_variant_id"] == ""
def test_chat_response_mirrors_register_fields():
"""ChatResponse exposes the same register fields as TurnEvent."""
runtime = ChatRuntime(
config=RuntimeConfig(register_pack_id="convivial_v1")
)
response = runtime.chat("What is light?")
event = runtime.turn_log[-1]
assert response.register_id == event.register_id == "convivial_v1"
assert response.register_variant_id == event.register_variant_id

View file

@ -0,0 +1,72 @@
"""Register tour demo — load-bearing seam claim (ADR-0072 / R5).
Pins the three seam claims:
* grounding_source identical across {neutral, terse, convivial} per prompt
* trace_hash identical across {neutral, terse, convivial} per prompt
* surface differs at least once (convivial vs neutral)
Any one of those failing is the R5 architectural-regression signal.
"""
from __future__ import annotations
from evals.register_tour.run_tour import _PROMPTS, _REGISTERS, run_tour
def test_tour_returns_structured_report():
report = run_tour(emit_json=True)
assert set(report) >= {
"registers", "prompts", "grid", "claims", "all_claims_supported",
}
assert list(report["registers"]) == list(_REGISTERS)
assert list(report["prompts"]) == list(_PROMPTS)
def test_tour_grounding_sources_identical_across_registers():
report = run_tour(emit_json=True)
assert report["claims"]["all_grounding_sources_identical"] is True
def test_tour_trace_hashes_identical_across_registers():
"""ADR-0069 invariant C, restated as a falsifiable demo claim."""
report = run_tour(emit_json=True)
assert report["claims"]["all_trace_hashes_identical"] is True
def test_tour_surfaces_vary_at_least_once():
"""ADR-0071 seeded variation: convivial vs neutral must differ
somewhere across the four-prompt sequence."""
report = run_tour(emit_json=True)
assert report["claims"]["surfaces_vary_at_least_once"] is True
def test_tour_all_claims_supported():
"""Canonical R5 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_register_id_per_cell():
"""Each grid cell records the register_id that produced it."""
report = run_tour(emit_json=True)
for register_id in _REGISTERS:
cells = report["grid"][register_id]
assert len(cells) == len(_PROMPTS)
for cell in cells:
assert cell["register_id"] == register_id
def test_tour_grid_variant_id_empty_when_no_decoration():
"""terse_v1 + default_neutral_v1 emit empty variant_ids; convivial_v1
emits non-empty variant_ids on non-empty surfaces."""
report = run_tour(emit_json=True)
for cell in report["grid"]["default_neutral_v1"]:
assert cell["register_variant_id"] == ""
for cell in report["grid"]["terse_v1"]:
assert cell["register_variant_id"] == ""
convivial_non_empty = [
c for c in report["grid"]["convivial_v1"]
if c["surface"]
]
assert any(c["register_variant_id"] != "" for c in convivial_non_empty)

View file

@ -4,7 +4,12 @@
from __future__ import annotations
from chat.register_variation import _select_bucket_entry, decorate_surface
from chat.register_variation import (
DecorationResult,
_select_bucket_entry,
decorate_surface,
decorate_surface_str,
)
from packs.register.loader import UNREGISTERED, load_register_pack
@ -120,19 +125,25 @@ def test_decorate_surface_empty_buckets_noop():
"""UNREGISTERED / neutral / terse all have empty buckets."""
surface = "Light is illumination. Pack-grounded (en_core_cognition_v1)."
out = decorate_surface(surface, UNREGISTERED, turn_idx=0)
assert out == surface
assert isinstance(out, DecorationResult)
assert out.surface == surface
assert out.variant_id == ""
neutral = load_register_pack("default_neutral_v1")
assert decorate_surface(surface, neutral, turn_idx=0) == surface
assert decorate_surface(surface, neutral, turn_idx=0).surface == surface
assert decorate_surface(surface, neutral, turn_idx=0).variant_id == ""
terse = load_register_pack("terse_v1")
assert decorate_surface(surface, terse, turn_idx=0) == surface
assert decorate_surface(surface, terse, turn_idx=0).surface == surface
assert decorate_surface(surface, terse, turn_idx=0).variant_id == ""
def test_decorate_surface_empty_input_noop():
"""Empty input ⇒ empty output regardless of register."""
convivial = load_register_pack("convivial_v1")
assert decorate_surface("", convivial, turn_idx=0) == ""
out = decorate_surface("", convivial, turn_idx=0)
assert out.surface == ""
assert out.variant_id == ""
def test_decorate_surface_convivial_attaches_markers():
@ -143,8 +154,11 @@ def test_decorate_surface_convivial_attaches_markers():
out = decorate_surface(surface, convivial, turn_idx=0)
# The opening (3 entries, no empty) is always non-empty.
# Therefore the surface must change.
assert out != surface
assert surface in out
assert out.surface != surface
assert surface in out.surface
# ADR-0072 (R5) — non-empty markers produce a non-empty variant_id.
assert out.variant_id != ""
assert len(out.variant_id) == 12
def test_decorate_surface_is_deterministic():
@ -160,7 +174,61 @@ def test_decorate_surface_turn_idx_varies_output():
surface = "Light is illumination."
seen: set[str] = set()
for t in range(15):
seen.add(decorate_surface(surface, convivial, turn_idx=t))
seen.add(decorate_surface(surface, convivial, turn_idx=t).surface)
# 15 turns × 3 openings × 3 closings = 9 possible outputs;
# should see at least 4 distinct.
assert len(seen) >= 4, f"only {len(seen)} distinct decorations across 15 turns"
# ADR-0072 (R5) — DecorationResult + variant_id + str alias.
def test_decorate_surface_str_alias_returns_string():
"""decorate_surface_str preserves the pre-R5 string-only API."""
convivial = load_register_pack("convivial_v1")
surface = "Light is illumination."
out = decorate_surface_str(surface, convivial, turn_idx=0)
assert isinstance(out, str)
full = decorate_surface(surface, convivial, turn_idx=0)
assert out == full.surface
def test_variant_id_stable_across_invocations():
"""Same (opening, closing) pair ⇒ identical 12-char variant_id."""
convivial = load_register_pack("convivial_v1")
surface = "Light is illumination."
a = decorate_surface(surface, convivial, turn_idx=0)
b = decorate_surface(surface, convivial, turn_idx=0)
assert a.variant_id == b.variant_id
# Stable hex prefix of 12 chars.
assert len(a.variant_id) == 12
int(a.variant_id, 16) # raises if not hex
def test_variant_id_distinguishes_distinct_marker_pairs():
"""Different marker selections ⇒ different variant_ids."""
convivial = load_register_pack("convivial_v1")
surface = "Light is illumination."
seen_pairs: set[tuple[str, str]] = set()
seen_variant_ids: set[str] = set()
for t in range(40):
result = decorate_surface(surface, convivial, turn_idx=t)
seen_pairs.add((result.opening, result.closing))
if result.variant_id:
seen_variant_ids.add(result.variant_id)
# Bijection between non-trivial pairs and variant_ids:
# the number of distinct variant_ids equals the number of
# distinct non-empty pairs (collisions on 48 bits ≈ 2^-48 ⇒ none).
non_trivial_pairs = {p for p in seen_pairs if p != ("", "")}
assert len(seen_variant_ids) == len(non_trivial_pairs)
def test_variant_id_empty_for_no_decoration():
"""UNREGISTERED / neutral / terse always emit ``variant_id=""``."""
surface = "Light is illumination."
for pack_id in ("default_neutral_v1", "terse_v1"):
pack = load_register_pack(pack_id)
for t in range(5):
assert decorate_surface(surface, pack, turn_idx=t).variant_id == ""
for t in range(5):
assert decorate_surface(surface, UNREGISTERED, turn_idx=t).variant_id == ""