Introduces the presentation axis as a fourth pack class (sibling to identity /
safety / ethics), orthogonal to the truth path. Same input + same packs +
same register ⇒ bit-for-bit reproducible surface; varying any of the three ⇒
genuinely different output. No stochastic sampling.
ADR-0068 (R1): RegisterPack frozen dataclass, loader, ratify script, seam test.
- default_neutral_v1 ratified as null register.
ADR-0069 (R2): realizer register parameter threaded through 9 composer entry
points; RuntimeConfig.register_pack_id; three byte-identity invariants
(A: None ≡ pre-R2 unregistered; B: None ≡ default_neutral_v1; C: trace_hash
invariant under register). Amended to default-with-lint after 167-call-site
scout: composers default to UNREGISTERED, AST lint enforces explicit
register= at runtime call sites.
ADR-0070 (R3): terse_v1 register, first non-neutral pack. realizer_overrides
schema with known-keys allow-list (disclosure_domain_count ∈ {1,2,3}).
build_pack_surface_candidate reads override with fail-soft clamp. New
invariant register_invariant_grounding asserts grounding_source +
trace_hash byte-identical across {None, neutral, terse}.
ADR-0071 (R4): seeded surface variation via convivial_v1.
chat/register_variation.py applies SHA-256-seeded marker selection from
bounded discourse-marker buckets. ChatResponse.pre_decoration_surface routes
truth-path surface to core/cognition/pipeline.py so trace_hash stays
invariant under register (the load-bearing architectural fix — initially
invariant C failed under convivial because decoration was leaking into
trace_hash via response.surface). Empty-string marker entries now
legitimate ("no marker this turn" is a valid seed pick). realizer_overrides
schema widened with per_intent nested block (validated against IntentTag
whitelist; wired but not exercised by convivial). Two new invariants:
seeded_variation_replay_equivalence (fresh runtimes → byte-identical) and
seeded_variation_turn_distinct (same prompt across turns → ≥2 distinct
surfaces).
ADR-0072 (R5, draft): telemetry + operator surface — TurnEvent gains
register_id and register_variant_id, core chat --register flag, core demo
register-tour. Status: Proposed; not yet implemented.
Three ratified register packs ship: default_neutral_v1 (null), terse_v1
(disclosure_domain_count=1), convivial_v1 (3 openings × 3 closings).
Verification:
- 84 register tests pass + 1 documented skip
- Curated lanes green: smoke 67, cognition 120+1s, teaching 17, packs 6,
runtime 19, algebra 132
- Cognition eval byte-identical to pre-register baseline:
public 100/100/91.7/100, holdout 100/100/83.3/100
- Full lane: 2608 passed, 4 skipped, 1 failed (pre-existing
test_cli_demo.py "Combined Demo" → "Run Every Demo" rename, unrelated)
Truth-path isolation: chat/register_variation.py is realizer-side; the seam
test (tests/test_register_pack_seam.py) refuses imports of packs.register
from intent classification, propagation, vault recall, trace hashing, and
algebra.
166 lines
5.5 KiB
Python
166 lines
5.5 KiB
Python
"""Seeded surface variation — selector determinism + decoration shape
|
||
(ADR-0071, Plan Phase R4).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from chat.register_variation import _select_bucket_entry, decorate_surface
|
||
from packs.register.loader import UNREGISTERED, load_register_pack
|
||
|
||
|
||
def test_selector_returns_empty_string_for_empty_bucket():
|
||
result = _select_bucket_entry(
|
||
(),
|
||
seed_text="anything",
|
||
register_id="test_v1",
|
||
turn_idx=0,
|
||
bucket_name="openings",
|
||
)
|
||
assert result == ""
|
||
|
||
|
||
def test_selector_is_deterministic():
|
||
bucket = ("a", "b", "c", "d", "e")
|
||
args = dict(
|
||
seed_text="some surface", register_id="test_v1",
|
||
turn_idx=7, bucket_name="openings",
|
||
)
|
||
first = _select_bucket_entry(bucket, **args)
|
||
for _ in range(50):
|
||
assert _select_bucket_entry(bucket, **args) == first
|
||
|
||
|
||
def test_selector_varies_with_turn_idx():
|
||
bucket = ("a", "b", "c", "d", "e")
|
||
seen = set()
|
||
for turn_idx in range(20):
|
||
seen.add(_select_bucket_entry(
|
||
bucket,
|
||
seed_text="constant",
|
||
register_id="test_v1",
|
||
turn_idx=turn_idx,
|
||
bucket_name="openings",
|
||
))
|
||
# 20 turns over a 5-entry bucket should hit at least 3 entries.
|
||
assert len(seen) >= 3, (
|
||
f"seed is not varying with turn_idx — only saw {seen}"
|
||
)
|
||
|
||
|
||
def test_selector_varies_with_register_id():
|
||
bucket = ("a", "b", "c", "d", "e")
|
||
a = _select_bucket_entry(
|
||
bucket, seed_text="same", register_id="reg_A",
|
||
turn_idx=0, bucket_name="openings",
|
||
)
|
||
b = _select_bucket_entry(
|
||
bucket, seed_text="same", register_id="reg_B",
|
||
turn_idx=0, bucket_name="openings",
|
||
)
|
||
# Not guaranteed for a single bucket size 5 — sweep a few
|
||
# turn_idx values to ensure register_id moves the seed at all.
|
||
diffs = 0
|
||
for t in range(20):
|
||
a_t = _select_bucket_entry(
|
||
bucket, seed_text="same", register_id="reg_A",
|
||
turn_idx=t, bucket_name="openings",
|
||
)
|
||
b_t = _select_bucket_entry(
|
||
bucket, seed_text="same", register_id="reg_B",
|
||
turn_idx=t, bucket_name="openings",
|
||
)
|
||
if a_t != b_t:
|
||
diffs += 1
|
||
assert diffs > 0, (
|
||
f"register_id has no effect on selector across 20 turn_idx "
|
||
f"values (first picks: a={a!r}, b={b!r})"
|
||
)
|
||
|
||
|
||
def test_selector_distributes_across_bucket():
|
||
"""Across many distinct seed_text inputs, every entry should be
|
||
selected at least once."""
|
||
bucket = ("a", "b", "c")
|
||
seen: set[str] = set()
|
||
for i in range(300):
|
||
seen.add(_select_bucket_entry(
|
||
bucket,
|
||
seed_text=f"surface_{i}",
|
||
register_id="test",
|
||
turn_idx=0,
|
||
bucket_name="openings",
|
||
))
|
||
assert seen == {"a", "b", "c"}
|
||
|
||
|
||
def test_selector_uses_bucket_name():
|
||
"""Different bucket_name keys produce different seeds."""
|
||
bucket = ("a", "b", "c", "d", "e", "f", "g", "h")
|
||
args = dict(seed_text="x", register_id="r", turn_idx=0)
|
||
o = _select_bucket_entry(bucket, bucket_name="openings", **args)
|
||
c = _select_bucket_entry(bucket, bucket_name="closings", **args)
|
||
diffs = 0
|
||
for t in range(20):
|
||
o_t = _select_bucket_entry(
|
||
bucket, seed_text="x", register_id="r",
|
||
turn_idx=t, bucket_name="openings",
|
||
)
|
||
c_t = _select_bucket_entry(
|
||
bucket, seed_text="x", register_id="r",
|
||
turn_idx=t, bucket_name="closings",
|
||
)
|
||
if o_t != c_t:
|
||
diffs += 1
|
||
# Not strict on (o, c) at turn 0 alone — sweep guards.
|
||
_ = (o, c)
|
||
assert diffs > 0
|
||
|
||
|
||
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
|
||
|
||
neutral = load_register_pack("default_neutral_v1")
|
||
assert decorate_surface(surface, neutral, turn_idx=0) == surface
|
||
|
||
terse = load_register_pack("terse_v1")
|
||
assert decorate_surface(surface, terse, turn_idx=0) == surface
|
||
|
||
|
||
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) == ""
|
||
|
||
|
||
def test_decorate_surface_convivial_attaches_markers():
|
||
"""Convivial register has populated openings and closings — at
|
||
least one of them should attach to most surfaces."""
|
||
convivial = load_register_pack("convivial_v1")
|
||
surface = "Light is illumination."
|
||
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
|
||
|
||
|
||
def test_decorate_surface_is_deterministic():
|
||
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 == b
|
||
|
||
|
||
def test_decorate_surface_turn_idx_varies_output():
|
||
convivial = load_register_pack("convivial_v1")
|
||
surface = "Light is illumination."
|
||
seen: set[str] = set()
|
||
for t in range(15):
|
||
seen.add(decorate_surface(surface, convivial, turn_idx=t))
|
||
# 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"
|