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.
13 KiB
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 ofterse/standard/pedagogical. Consumed by future composed-surface depth modulation (ADR-0062 generalisation). v1 default packs usestandard.realizer_overrides— mappingintent → template_family_id. An empty mapping means "use the unregistered realizer template" for every intent. v1default_neutral_v1ships 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 thechecksumfield 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:
- The pack class has at least one ratified member at R1 close.
- The R2 wiring has an explicit "loaded but no-op" target to test
against (vs. the
Nonesentinel path). - 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
checksumpopulated. - Generates the
.mastery_report.jsonwithratification_method,evidenceblock, andreport_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 / invalidregister_idrejection / missing mastery report rejection in production mode / checksum mismatch rejection.test_register_pack_seam.py— pin the architectural commitment. Importscore/cognition/trace.py,generate/realizer.py,chat/runtime.py, and asserts none of them importcore/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_idis path-sanitised._safe_pack_idfromcore/_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 topacks/register/. ADR-0057 doctrine (corpus mutation only throughaccept_proposalorsupersede_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 assertscompose_boundary_idsignores any register pack id passed in. - Trace hash unchanged. Test asserts the trace hash of a turn is
invariant under
RuntimeConfig.register_pack_idonce 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_idlive inRuntimeConfig? 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_v1can 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.pyto accept(target, register=None) → surface. AddRuntimeConfig.register_pack_id. CI-pin null-lift invariant:register_pack_id=None≡register_pack_id="default_neutral_v1"≡ current unregistered output, byte-for-byte. - ADR-0070 (Phase R3) — second ratified register pack
(
terse_v1candidate). First non-neutral register. Ratification gate: cognition lane passes on both registers; new metricregister_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_idandtemplate_variant_id.core chat --register <id>flag.core demo register-tour— three-scene demo: identical grounding evidence, three surfaces.