core/docs/decisions/ADR-0068-register-pack-class.md
Shay 6207b5fd0e feat(register): R1–R4 register pack subsystem — deterministic surface variation
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.
2026-05-19 16:52:36 -07:00

13 KiB
Raw Permalink Blame History

ADR-0068 — Register pack class (Plan Phase R1)

Status: Accepted Date: 2026-05-19 Ratified: 2026-05-19 Author: Shay Phase: Plan Phase R1 (presentation-axis foundation) Builds on: ADR-0027 (identity packs) / ADR-0029 (safety pack) / ADR-0033 (ethics pack) / ADR-0048 (pack-grounded surface) / ADR-0062 (composed surface)


Context

CORE's truth path is deterministic by construction: intent classification → proposition graph → articulation target → realizer → trace hash. Replay is bit-exact. The cognition lane is CI-pinned to byte-identity across flag-off / flag-on for every additive ADR since 0048.

The accepted critique of deterministic systems is not that they are wrong, but that they can feel rigid: same meaning, same wording, same cadence. A user reads identical surfaces across turns as scripted rather than thoughtful. The reflexive fix — add sampling to generation — is forbidden by CLAUDE.md (no stochastic fallback, no hidden normalization, trace hash must remain stable).

The framing this ADR adopts:

The problem is not deterministic cognition; the problem is a single deterministic rendering channel.

Variation in how a resolved answer is rendered does not require variation in what was resolved. Register, cadence, depth, and discourse-marker choice are orthogonal to the proposition graph, grounding source, and chain resolution. They belong on a separate axis — a presentation axis — and that axis can itself be deterministic given a wider input space.

This ADR introduces the data shape for that axis. It deliberately ships no runtime wiring. The realizer is untouched. The cognition lane must remain byte-identical after this ADR lands, because nothing consumes register packs yet.


Decision

Introduce a fourth pack-layer class: register packs. Structurally a sibling to identity / safety / ethics packs (swappable, ratified, checksum-sealed), but architecturally distinct in where they compose:

identity  safety  ethics  →  boundary_ids        (manifold-side)
register                    →  realizer parameter  (surface-side)

A register pack does not union into the runtime manifold, does not contribute to boundary_ids, does not participate in safety / ethics verdicts, and does not affect grounding source. It parameterizes realizer template selection and discourse-marker insertion.

The load-bearing commitment

Register is a property of the realizer, not the proposition graph.

The trace hash is computed on the articulation target plus grounding evidence, before register is consulted. Replay equivalence (teaching/replay.py) continues to compare at the articulation-target level. Two surfaces that differ only in register render from the same target, the same grounding source, the same chain set, and the same trace hash.

If this seam slips — if register ever influences which chain resolves, which grounding source is selected, or which proposition admits — the invariant is gone. Tests in this ADR pin the seam.

Files

packs/register/                                               NEW
packs/register/__init__.py                                    NEW
packs/register/loader.py                                      NEW (mirrors packs/identity/loader.py)
packs/register/default_neutral_v1.json                        NEW
packs/register/default_neutral_v1.mastery_report.json         NEW (self-seal)
scripts/ratify_register_packs.py                              NEW (idempotent)
tests/test_register_pack_loader.py                            NEW
tests/test_register_pack_seam.py                              NEW
docs/decisions/ADR-0068-register-pack-class.md                NEW (this file)

The loader lives at packs/register/loader.py (sibling to packs/identity/loader.py, packs/safety/loader.py, packs/ethics/loader.py), not under core/packs/. This matches the existing pack-layer convention.

No changes to generate/realizer.py, chat/runtime.py, core/cognition/pipeline.py, core/cognition/trace.py, or any existing surface composer. Wiring is deferred to ADR-0069 (Phase R2).

Pack schema

{
  "register_id": "default_neutral_v1",
  "display_name": "Default neutral",
  "description": "Baseline register. Mirrors current realizer output.",
  "depth_preference": "standard",
  "realizer_overrides": {},
  "discourse_markers": {
    "openings": [],
    "transitions": [],
    "closings": []
  },
  "ratified_on": "2026-05-19",
  "checksum": "<sha256 of pack body bytes>"
}

Field semantics:

  • register_id — kebab-case unique id, validated via _safe_pack_id (ADR-0051 trust-boundary doctrine).
  • depth_preference — one of terse / standard / pedagogical. Consumed by future composed-surface depth modulation (ADR-0062 generalisation). v1 default packs use standard.
  • realizer_overrides — mapping intent → template_family_id. An empty mapping means "use the unregistered realizer template" for every intent. v1 default_neutral_v1 ships empty by design — its ratification gate is exactly byte-identity with the unregistered path.
  • discourse_markers — three bounded lists. v1 ships all empty. R4 introduces seeded selection from non-empty lists.
  • checksum — SHA-256 of the JSON body bytes excluding the checksum field itself, mirroring identity pack mastery-report doctrine.

Mastery report (self-seal)

Each register pack ships a companion .mastery_report.json:

{
  "register_id": "default_neutral_v1",
  "ratified_on": "2026-05-19",
  "ratification_method": "byte_identity_null_lift",
  "evidence": {
    "cognition_lane_byte_identical": true,
    "discourse_markers_empty": true,
    "realizer_overrides_empty": true
  },
  "checksum": "<sha256 of pack body>",
  "report_checksum": "<sha256 of this report body excluding report_checksum>"
}

The loader verifies the pack checksum and the mastery report checksum in production mode (same gate as identity packs, ADR-0027).

Loader contract

# core/packs/register.py
def load_register_pack(register_id: str) -> RegisterPack: ...
def list_available_register_packs() -> tuple[str, ...]: ...
def verify_register_pack_seal(register_id: str) -> bool: ...

load_register_pack(None) returns an explicit sentinel RegisterPack.unregistered() — frozen, empty, never serialised to disk. This is what the realizer will consume when RuntimeConfig.register_pack_id is None (R2). The sentinel is structurally identical to default_neutral_v1's in-memory form; the two paths are required by test to render byte-identically once R2 wires them.

v1 ratified pack

default_neutral_v1 is the null-register baseline. It is the register whose surfaces match the current unregistered output exactly. Its ratification evidence is the byte-identity check, not eval-lift numbers. This pack exists so that:

  1. The pack class has at least one ratified member at R1 close.
  2. The R2 wiring has an explicit "loaded but no-op" target to test against (vs. the None sentinel path).
  3. Non-neutral registers in R3 have a structural baseline to diff against during ratification.

No second register ships in this ADR. terse_v1 is the R3 work.

Ratification script

scripts/ratify_register_packs.py mirrors scripts/ratify_identity_packs.py:

  • Reads pack JSON, normalises field ordering, computes checksum.
  • Writes back the pack with checksum populated.
  • Generates the .mastery_report.json with ratification_method, evidence block, and report_checksum.
  • Idempotent: re-running produces byte-identical files.

Consequences

Capability unlocked at R1

None visible at runtime. R1 lands the data shape and the loader. Capability unlocks at R2 (realizer parameterization), R3 (second ratified register pack), R4 (seeded selection), R5 (telemetry + demo).

Cognition lane — byte-identical

Required and CI-pinned. Nothing in this ADR consumes register packs at runtime, so:

public:  intent 100% / surface 100% / term 91.7% / closure 100%
holdout: intent 100% / surface 100% / term 83.3% / closure 100%

must hold post-merge, exactly as pre-merge.

Test coverage

  • test_register_pack_loader.py — load / list / verify-seal / unregistered-sentinel structural identity / ratification idempotence / invalid register_id rejection / missing mastery report rejection in production mode / checksum mismatch rejection.
  • test_register_pack_seam.py — pin the architectural commitment. Imports core/cognition/trace.py, generate/realizer.py, chat/runtime.py, and asserts none of them import core/packs/register.py. This test fails the moment register leaks upstream of the realizer. Removed in R2 and replaced with a narrower seam test once the realizer legitimately imports the loader.

Trust boundaries

  • Register cannot mutate truth. Loader returns a frozen dataclass; no setter, no in-place mutation. Realizer (R2) receives it by value.
  • register_id is path-sanitised. _safe_pack_id from core/_safe_display.py (ADR-0051) rejects traversal and unsafe ids before filesystem access.
  • Checksum-sealed. Pack body and mastery report are both SHA-256-sealed; production loader verifies both.
  • No new mutation surface. Register packs are operator-authored and ratified through scripts/ratify_register_packs.py. There is no runtime path that writes to packs/register/. ADR-0057 doctrine (corpus mutation only through accept_proposal or supersede_chain) is unrelated — register packs are not corpora.
  • Manifold isolation. Register packs do not contribute to boundary_ids. Safety / ethics / identity composition is unchanged. A separate test asserts compose_boundary_ids ignores any register pack id passed in.
  • Trace hash unchanged. Test asserts the trace hash of a turn is invariant under RuntimeConfig.register_pack_id once R2 lands; for R1, asserts the trace-hash codepath has no import of the register loader.

Verification

tests/test_register_pack_loader.py                           N passed
tests/test_register_pack_seam.py                             N passed
scripts/ratify_register_packs.py                             idempotent
Curated lanes (must remain green):
  smoke / cognition / teaching / packs / runtime / algebra
Cognition eval byte-identical (public + holdout).
Full lane: zero new failures.

The ratification script must produce byte-identical output on a second run (idempotence gate). The mastery report evidence block must include cognition_lane_byte_identical: true — verified by running core eval cognition with no runtime change and confirming pre/post output equality.


Open questions deferred to later phases

  • Where exactly does register_pack_id live in RuntimeConfig? R2 question. R1 only requires the loader to exist.
  • How does composition work when a register pack and a domain ethics pack both want to soften a refusal? Out of scope. Refusal remediation is a manifold-side concern (ADR-0036 / ADR-0037 / ADR-0038). Register is surface-side only. They cannot collide because they operate on different fields of ChatResponse.
  • Can register affect grounding_source? No, by construction. Grounding source is computed before realizer is invoked. A test in R2 will pin this; in R1 the seam test is sufficient.
  • Do CORRECTION (ADR-0060) and PROCEDURE (ADR-0061) templates factor cleanly for register overrides? Open R2 question. May require template refactor before terse_v1 can ratify in R3.
  • Auto-detect user's preferred register from phrasing? Separate cognitive task (intent classification extension). Deferred — needs its own ADR, not bundled here.

Future ADRs unlocked

  • ADR-0069 (Phase R2) — realizer parameterization. Widen generate/realizer.py to accept (target, register=None) → surface. Add RuntimeConfig.register_pack_id. CI-pin null-lift invariant: register_pack_id=Noneregister_pack_id="default_neutral_v1" ≡ current unregistered output, byte-for-byte.
  • ADR-0070 (Phase R3) — second ratified register pack (terse_v1 candidate). First non-neutral register. Ratification gate: cognition lane passes on both registers; new metric register_invariant_grounding=True (grounding source identical across registers for every prompt).
  • ADR-0071 (Phase R4) — seeded surface variation. (trace_hash, register, turn_idx) → template_variant_within_family. Discourse markers and template families gain bounded variants. Replay still bit-exact given the same seed inputs.
  • ADR-0072 (Phase R5) — telemetry + operator surface. TurnEvent gains register_id and template_variant_id. core chat --register <id> flag. core demo register-tour — three-scene demo: identical grounding evidence, three surfaces.