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.
113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
"""Seeded surface variation (ADR-0071, Plan Phase R4).
|
|
|
|
Deterministic discourse-marker selection from bounded register-pack
|
|
buckets, keyed on ``(seed_text, register_id, turn_idx, bucket_name)``.
|
|
|
|
The ADR specifies ``trace_hash`` as the primary seed input. The
|
|
implementation passes the *pre-decoration surface* as ``seed_text``
|
|
instead, for pragmatic reasons:
|
|
|
|
* The pre-decoration surface is a deterministic downstream projection
|
|
of the truth path (intent → proposition graph → realizer → surface).
|
|
* It is already available at the decoration call site without
|
|
threading ``trace_hash`` through ``TurnEvent`` / ``ChatResponse``.
|
|
* There is no cycle: decoration reads the pre-decoration string and
|
|
writes a post-decoration string; the seed never sees its own output.
|
|
* Replay equivalence holds: same input sequence ⇒ same pre-decoration
|
|
surfaces ⇒ same decoration choices.
|
|
|
|
Trust boundary
|
|
--------------
|
|
This module READS surface text to seed marker selection. It MUST NOT
|
|
feed back into the truth path (intent / proposition graph / trace
|
|
hash). ADR-0069 invariant C and ADR-0070
|
|
``register_invariant_grounding`` continue to hold; the seam test
|
|
(``tests/test_register_pack_seam.py``) keeps truth-path modules free of
|
|
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.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
|
|
from packs.register.loader import RegisterPack
|
|
|
|
|
|
def _select_bucket_entry(
|
|
bucket: tuple[str, ...],
|
|
*,
|
|
seed_text: str,
|
|
register_id: str,
|
|
turn_idx: int,
|
|
bucket_name: str,
|
|
) -> str:
|
|
"""Deterministically select one entry from *bucket*, or ``''``.
|
|
|
|
Same inputs ⇒ same output, forever. Different ``turn_idx`` against
|
|
the same seed_text + register typically picks a different entry
|
|
(uniform across the seed space).
|
|
"""
|
|
if not bucket:
|
|
return ""
|
|
seed_bytes = (
|
|
f"{seed_text}|{register_id}|{turn_idx}|{bucket_name}"
|
|
).encode("utf-8")
|
|
digest = hashlib.sha256(seed_bytes).digest()
|
|
idx = int.from_bytes(digest[:8], "big") % len(bucket)
|
|
return bucket[idx]
|
|
|
|
|
|
def decorate_surface(
|
|
surface: str,
|
|
register: RegisterPack,
|
|
*,
|
|
turn_idx: int,
|
|
seed_text: str | None = None,
|
|
) -> str:
|
|
"""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").
|
|
|
|
``seed_text`` defaults to *surface* — the pre-decoration string is
|
|
the natural seed. Callers with a stronger deterministic key (e.g.
|
|
a turn-trace hash) may pass it explicitly.
|
|
|
|
Transitions are accepted by the schema but NOT consumed at R4;
|
|
they sit until a later phase that owns clause-boundary detection.
|
|
"""
|
|
if not surface:
|
|
return surface
|
|
if seed_text is None:
|
|
seed_text = surface
|
|
markers = register.discourse_markers
|
|
opening = _select_bucket_entry(
|
|
markers.openings,
|
|
seed_text=seed_text,
|
|
register_id=register.register_id,
|
|
turn_idx=turn_idx,
|
|
bucket_name="openings",
|
|
)
|
|
closing = _select_bucket_entry(
|
|
markers.closings,
|
|
seed_text=seed_text,
|
|
register_id=register.register_id,
|
|
turn_idx=turn_idx,
|
|
bucket_name="closings",
|
|
)
|
|
out = surface
|
|
if opening:
|
|
out = f"{opening} {out}"
|
|
if closing:
|
|
out = f"{out}{closing}"
|
|
return out
|
|
|
|
|
|
__all__ = ("decorate_surface",)
|