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.
324 lines
13 KiB
Markdown
324 lines
13 KiB
Markdown
# 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
|
||
|
||
```json
|
||
{
|
||
"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`:
|
||
|
||
```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
|
||
|
||
```python
|
||
# 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=None` ≡ `register_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.
|