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:
parent
6207b5fd0e
commit
7f0bad3e20
12 changed files with 744 additions and 23 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
35
core/cli.py
35
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 <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 "
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
evals/register_tour/__init__.py
Normal file
5
evals/register_tour/__init__.py
Normal 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.
|
||||
"""
|
||||
227
evals/register_tour/run_tour.py
Normal file
227
evals/register_tour/run_tour.py
Normal 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
|
||||
# R1–R4 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)
|
||||
93
tests/test_register_cli.py
Normal file
93
tests/test_register_cli.py
Normal 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
|
||||
113
tests/test_register_telemetry.py
Normal file
113
tests/test_register_telemetry.py
Normal 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
|
||||
72
tests/test_register_tour_demo.py
Normal file
72
tests/test_register_tour_demo.py
Normal 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)
|
||||
|
|
@ -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 == ""
|
||||
|
|
|
|||
Loading…
Reference in a new issue