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.
339 lines
13 KiB
Markdown
339 lines
13 KiB
Markdown
# ADR-0070 — Second ratified register pack: `terse_v1` (Plan Phase R3)
|
||
|
||
**Status:** Accepted
|
||
**Date:** 2026-05-19
|
||
**Ratified:** 2026-05-19
|
||
**Author:** Shay
|
||
**Phase:** Plan Phase R3 (first non-neutral register)
|
||
**Builds on:** ADR-0068 (register pack class), ADR-0069 (realizer register parameter)
|
||
|
||
---
|
||
|
||
## Context
|
||
|
||
ADR-0068 landed the register pack class and ratified `default_neutral_v1`
|
||
(the null register). ADR-0069 wired a `RegisterPack` parameter through
|
||
every realizer-side composer and pinned three byte-identity invariants
|
||
(A/B/C) so the cognition lane is provably unchanged under the
|
||
unregistered sentinel and the null pack.
|
||
|
||
R3 is the first ADR where surface output is *allowed* to differ. The
|
||
load-bearing question this phase answers is:
|
||
|
||
> Can a non-neutral register produce visibly different surfaces while
|
||
> the **grounding source** (vault / pack / teaching / none) and the
|
||
> **trace hash** remain invariant for every prompt in the cognition
|
||
> lane?
|
||
|
||
If yes, we have proven that register is genuinely a property of the
|
||
realizer, not the truth path. If no, the seam leaks and R3 reverts.
|
||
|
||
R3 ships a single concrete knob (`disclosure_domain_count`), the
|
||
minimum surface area needed to make the answer testable.
|
||
|
||
---
|
||
|
||
## Decision
|
||
|
||
Ratify a second register pack, `terse_v1`, with one non-default
|
||
`realizer_overrides` key: `disclosure_domain_count = 1`. Widen the
|
||
without-gloss disclosure path in `chat/pack_grounding.py` to consult
|
||
this override. Pin a new CI invariant —
|
||
`register_invariant_grounding` — across every cognition lane case.
|
||
|
||
### The new pack
|
||
|
||
```json
|
||
{
|
||
"register_id": "terse_v1",
|
||
"version": "1.0.0",
|
||
"description": "Terse register. Compresses without-gloss disclosure to one semantic domain. Grounding source and trace hash invariant against default_neutral_v1.",
|
||
"schema_version": "1.0.0",
|
||
"display_name": "Terse",
|
||
"depth_preference": "terse",
|
||
"realizer_overrides": {
|
||
"disclosure_domain_count": 1
|
||
},
|
||
"discourse_markers": {
|
||
"openings": [],
|
||
"transitions": [],
|
||
"closings": []
|
||
}
|
||
}
|
||
```
|
||
|
||
`discourse_markers` stay empty — discourse-marker insertion is R4's
|
||
job. `depth_preference` is the operator-facing label; the
|
||
*behavioural* knob is the explicit `realizer_overrides` entry. The
|
||
loader continues to refuse to load an unratified pack without
|
||
`mastery_report_sha256` and the companion self-sealed report.
|
||
|
||
### The R3 ratification gate
|
||
|
||
The R1/R2 gate (`scripts/ratify_register_packs.py`) refused any pack
|
||
that was not structurally null (empty `realizer_overrides`, empty
|
||
`discourse_markers`). R3 widens the gate to:
|
||
|
||
```
|
||
1. Accept non-null realizer_overrides, but only known keys.
|
||
2. The set of known keys at R3 is exactly:
|
||
- disclosure_domain_count: int in {1, 2, 3}
|
||
3. Reject any unknown key with the offending key name in the error.
|
||
4. Discourse markers must remain empty at R3 (R4 widens this).
|
||
5. The self-seal procedure is unchanged from R1.
|
||
```
|
||
|
||
The "known keys" allow-list is the trust boundary against arbitrary
|
||
operator-authored data driving realizer dispatch. R4 widens the
|
||
allow-list explicitly per ADR.
|
||
|
||
### The composer change
|
||
|
||
Exactly one site changes — the without-gloss disclosure in
|
||
`build_pack_surface_candidate` in `chat/pack_grounding.py`:
|
||
|
||
```python
|
||
# Today (ADR-0048):
|
||
head = "; ".join(domains[:3])
|
||
|
||
# At R3:
|
||
n = register.realizer_overrides.get("disclosure_domain_count", 3)
|
||
head = "; ".join(domains[:n])
|
||
```
|
||
|
||
The default (`3`) is the pre-R3 behaviour, so the sentinel and
|
||
`default_neutral_v1` produce identical surfaces to pre-R3 output
|
||
(invariants A and B continue to hold trivially because both registers
|
||
have `disclosure_domain_count` either absent or unset).
|
||
|
||
The `pack_grounded_surface` entry point already threads `register`
|
||
through (R2), so no further signature change is needed. The downstream
|
||
callers — `build_pack_surface_candidate`'s wrappers in the comparison /
|
||
correction / procedure paths — already use `domains[:N]` slices with
|
||
their own counts (`:3` and `:2`), and those slices stay at their
|
||
existing values for R3. The single knob is `pack_grounded_surface`'s
|
||
without-gloss disclosure, deliberately.
|
||
|
||
### The new invariant
|
||
|
||
`register_invariant_grounding` joins A/B/C as the load-bearing CI gate
|
||
for R3:
|
||
|
||
```
|
||
invariant_register_grounding:
|
||
For every case in the cognition lane (public + holdout),
|
||
grounding_source(case) is identical across:
|
||
- register_pack_id = None
|
||
- register_pack_id = "default_neutral_v1"
|
||
- register_pack_id = "terse_v1"
|
||
```
|
||
|
||
Surface text is allowed to differ between `terse_v1` and the two null
|
||
registers (that is the whole point of R3). `grounding_source` is not.
|
||
If terse_v1 routes a case to a different grounding source, the seam is
|
||
leaking and the merge is blocked.
|
||
|
||
Invariants A/B/C from ADR-0069 continue to hold without modification —
|
||
they only compare the two null registers (None ≡ neutral) and never
|
||
involve terse_v1. The terse_v1 path adds the fourth invariant on top.
|
||
|
||
### Files
|
||
|
||
```
|
||
packs/register/terse_v1.json NEW
|
||
packs/register/terse_v1.mastery_report.json NEW (generated)
|
||
scripts/ratify_register_packs.py EDIT
|
||
- Widen R1 null-only gate to "non-null overrides allowed,
|
||
known keys only, discourse_markers must stay empty"
|
||
- Known-key allow-list: {"disclosure_domain_count"} at R3
|
||
|
||
chat/pack_grounding.py EDIT
|
||
- build_pack_surface_candidate reads
|
||
register.realizer_overrides.get("disclosure_domain_count", 3)
|
||
- Threaded into the without-gloss head construction
|
||
- No other composer site touched
|
||
|
||
tests/test_register_pack_terse_v1.py NEW
|
||
- terse_v1 loads, self-seal verifies, depth_preference == "terse"
|
||
- realizer_overrides == {"disclosure_domain_count": 1}
|
||
- pack_grounded_surface(lemma, register=terse) emits one-domain head
|
||
- pack_grounded_surface(lemma, register=neutral) emits three-domain head
|
||
- Both surfaces carry the same pack-grounded({pack_id}) provenance
|
||
|
||
tests/test_register_invariant_grounding.py NEW
|
||
- Runs the cognition lane three ways
|
||
(None / default_neutral_v1 / terse_v1)
|
||
- Asserts grounding_source identical case-by-case
|
||
- Asserts trace_hash invariant across all three (extends invariant C)
|
||
- Asserts versor closure rate identical across all three
|
||
|
||
docs/decisions/ADR-0070-register-pack-terse-v1.md NEW (this file)
|
||
```
|
||
|
||
### Wiring shape
|
||
|
||
```python
|
||
# chat/pack_grounding.py — build_pack_surface_candidate
|
||
def build_pack_surface_candidate(
|
||
lemma: str,
|
||
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
|
||
*,
|
||
register: RegisterPack = UNREGISTERED,
|
||
) -> PackSurfaceCandidate | None:
|
||
...
|
||
# without-gloss disclosure path
|
||
n = register.realizer_overrides.get("disclosure_domain_count", 3)
|
||
if not isinstance(n, int) or n < 1 or n > 3:
|
||
# Defensive: ratification gate already enforces this, but
|
||
# the realizer hot path must not crash on malformed data.
|
||
n = 3
|
||
head = "; ".join(domains[:n])
|
||
...
|
||
```
|
||
|
||
The defensive `1 ≤ n ≤ 3` clamp duplicates the ratification gate's
|
||
allow-list. Acceptable belt-and-suspenders because the realizer hot
|
||
path is invoked once per turn and a crash here would block the entire
|
||
session. The ratification gate stays the authoritative trust boundary;
|
||
the clamp is a fail-soft for off-path callers.
|
||
|
||
---
|
||
|
||
## Consequences
|
||
|
||
### Capability unlocked at R3
|
||
|
||
The first non-neutral register, end-to-end. An operator can now boot
|
||
`ChatRuntime` with `RuntimeConfig(register_pack_id="terse_v1")` and
|
||
receive measurably shorter without-gloss surfaces while the grounding
|
||
source and trace hash are unchanged.
|
||
|
||
This is what R3 is *for*: prove the seam holds when something
|
||
actually moves.
|
||
|
||
### Cognition lane — split expectation
|
||
|
||
```
|
||
None : unchanged from R2 baseline
|
||
neutral : unchanged from R2 baseline (invariant B)
|
||
terse : surface_grounded MAY differ; intent / term-capture /
|
||
versor closure MUST match neutral byte-for-byte
|
||
UNLESS surface_grounded depends on disclosure word count
|
||
(it does not — substring tests look for "pack-grounded")
|
||
```
|
||
|
||
The R3 expectation is that terse_v1 lane numbers will equal neutral
|
||
lane numbers, because the cognition lane's surface predicates pin
|
||
provenance markers (`pack-grounded ({pack_id})`) and lemma presence,
|
||
not full disclosure clauses. If a case fails under terse but passes
|
||
under neutral, the cognition lane fixture has a latent dependency on
|
||
the third disclosure domain — useful debugging signal, but should not
|
||
gate R3.
|
||
|
||
### Test coverage
|
||
|
||
- `test_register_pack_terse_v1.py` — direct load + composer
|
||
invocation under both registers.
|
||
- `test_register_invariant_grounding.py` — the load-bearing artifact.
|
||
Cognition lane × three registers × grounding-source diff.
|
||
- Existing tests (`test_register_null_lift.py`, etc.) continue to
|
||
pass without modification because they pair only None and neutral.
|
||
|
||
### Performance
|
||
|
||
`register.realizer_overrides.get(key, default)` is a dict lookup per
|
||
turn (one composer site). Negligible. The pack is loaded once at
|
||
session start and passed by reference.
|
||
|
||
### Trust boundaries
|
||
|
||
- **Operator-authored data cannot drive arbitrary realizer
|
||
dispatch.** The ratification gate's known-key allow-list is the
|
||
defence-in-depth: a pack file claiming `realizer_overrides:
|
||
{"exec_python": "..."}` cannot ratify. R4 will widen the allow-list
|
||
with the same gate.
|
||
- **Defensive realizer clamp** is fail-soft, not a primary trust
|
||
surface. Removing it would not change correctness — the gate would
|
||
have rejected the pack — but it would change failure mode from
|
||
"shorter disclosure" to "IndexError on next turn." Fail-soft is
|
||
preferable in the realizer hot path.
|
||
- **Self-seal still verified.** `verify_register_pack_seal` is
|
||
unchanged; terse_v1 ships with its own
|
||
`terse_v1.mastery_report.json` produced by the ratify script.
|
||
- **No new mutation surface.** No runtime write path to
|
||
`packs/register/`. Operator-authored pack files only.
|
||
- **Path-traversal protection unchanged.** Pack IDs continue through
|
||
`_validate_pack_id` (ADR-0051).
|
||
|
||
### Replay determinism
|
||
|
||
`disclosure_domain_count` is a pack-resident integer. The surface for
|
||
a given `(lemma, register_pack_id)` is a deterministic function of
|
||
the ratified pack file. Replay against the same packs produces
|
||
identical output. Replay against a session that used a different
|
||
register produces different surfaces — but the trace hash and
|
||
grounding source remain invariant, so the cognition turn is still
|
||
fully reproducible from `(input, pack_set, register_pack_id)`.
|
||
|
||
---
|
||
|
||
## Verification
|
||
|
||
```
|
||
tests/test_register_pack_terse_v1.py N passed
|
||
tests/test_register_invariant_grounding.py N passed
|
||
tests/test_register_pack_seam.py unchanged, passes
|
||
tests/test_register_runtime_threading.py unchanged, passes
|
||
tests/test_register_null_lift.py unchanged, passes
|
||
Curated lanes (must remain green):
|
||
smoke / cognition / teaching / packs / runtime / algebra
|
||
Cognition eval:
|
||
register_pack_id=None == R2 baseline (byte-identical)
|
||
register_pack_id="default_neutral_v1" == None (invariant B)
|
||
register_pack_id="terse_v1" grounding_source / trace_hash /
|
||
versor_closures byte-identical
|
||
against neutral; surface MAY differ
|
||
on without-gloss disclosure cases
|
||
```
|
||
|
||
The new invariant test is the load-bearing R3 artifact. If it passes,
|
||
the seam survives its first stress test.
|
||
|
||
---
|
||
|
||
## Trust boundaries (summary)
|
||
|
||
- Ratification gate widened with explicit known-key allow-list.
|
||
- Realizer hot path defends against malformed data via clamp.
|
||
- Self-seal verification preserved.
|
||
- Path-traversal protection unchanged.
|
||
- No new runtime write surface.
|
||
|
||
---
|
||
|
||
## Open questions deferred to later phases
|
||
|
||
- **Per-intent overrides** (e.g. `realizer_overrides:
|
||
{"DEFINITION": {...}, "COMPARISON": {...}}`)? Deferred. R3's flat
|
||
schema is enough to prove the seam; per-intent dispatch is R4
|
||
alongside discourse markers.
|
||
- **Should `terse_v1` also compress the comparison surface
|
||
(`contrasts with`) or correction surface?** Deferred. R3 is one
|
||
knob, one composer. Multi-composer override is R4.
|
||
- **Should the runtime auto-resolve `register_pack_id=None` to
|
||
`default_neutral_v1`?** Still open from ADR-0069's open
|
||
questions. R3 does not change the answer. Sentinel and pack remain
|
||
structurally equivalent; the resolution question stays deferred to
|
||
R4 or later.
|
||
|
||
---
|
||
|
||
## Future ADRs unlocked
|
||
|
||
- **ADR-0071 (Phase R4)** — seeded surface variation +
|
||
discourse-marker insertion + per-intent realizer overrides.
|
||
`(trace_hash, register, turn_idx) → template_variant`. Widens
|
||
ratification gate further.
|
||
- **ADR-0072 (Phase R5)** — telemetry + operator surface.
|
||
`TurnEvent` gains `register_id` and `template_variant_id`. `core
|
||
chat --register <id>` flag. `core demo register-tour`.
|