R5 (ADR-0072) shipped the register *machinery*; ADR-0074's orthogonality tour proved the axis was decoratively orthogonal to anchor-lens but inspection of the cognition-eval surfaces revealed two structural gaps: * On pack-grounded DEFINITION/RECALL/COMPARISON composers, the only realizer override any register consumed was `disclosure_domain_count` — which only fires on the no-gloss disclosure path. Under terse_v1, every gloss-DEFINITION cell was byte-identical to default_neutral_v1. * The register-tour's `surfaces_vary_at_least_once` gate could be satisfied by convivial's decorative wrapper alone, masking that regression in CI. R6 closes both: Layering separation (the load-bearing fix): * New TurnEvent/ChatResponse field `register_canonical_surface` carries the composer output BEFORE any register transformation. The pipeline hashes this field for `trace_hash`, preserving R5's invariant that per-prompt trace_hash is CONSTANT across registers even while substantive transforms produce visibly different surfaces. Substantive transforms (`chat/register_substantive.py`): * terse_v1 gains 3 bool knobs: `drop_provenance_tag`, `compress_gloss`, `drop_articles` — all pure regex transforms on the canonical surface. * convivial_v1 gains `append_semantic_domain_clause` — appends a single bounded "Related: <atom>." clause using the lemma's pack atoms. * default_neutral_v1 leaves overrides empty; substantive transform is byte-identical no-op (preserves `byte_identity_null_lift`). * C1 (ADR-0075) safety preserved: drop_articles refuses to drop articles following `not` (avoids R3 violations); no knob combination trips R2/R3. Strengthened tour gate (`evals/register_tour/run_tour.py`): * Replaces `surfaces_vary_at_least_once` with two falsifiable claims: - `terse_substantively_differs_from_neutral_on_pack_grounded_definition` - `convivial_substantively_differs_from_neutral_on_pack_grounded_definition` Both restrict to DEFINITION+pack-grounded cells and require difference beyond whitespace/punctuation. * New claim `register_canonical_surfaces_identical` directly proves the layering separation. * Preserves R5's `all_grounding_sources_identical` + `all_trace_hashes_identical`. Pack ratification: * Loader widened to accept `bool` for closed-set R6 keys (drop_provenance_tag / compress_gloss / drop_articles / append_semantic_domain_clause). * `_KNOWN_OVERRIDE_KEYS` ratify gate extended with same. * terse_v1 + convivial_v1 reratified with new knobs; companion mastery reports re-sealed. default_neutral_v1 unchanged. Invariants pinned: * `invariant_register_canonical_surface_constant_across_registers` (new) * `invariant_terse_substantively_distinct_from_neutral` (new) * `invariant_convivial_substantively_distinct_from_neutral` (new) * `invariant_realizer_no_illegal_articulation` (C1, preserved) * `invariant_realizer_guard_byte_identity_on_currently_passing_cases` (C1, preserved) Verification: * `core eval cognition`: 100.0% / 91.7% / 100.0% / 100.0% — byte- identical under default_neutral_v1. * `core demo register-tour`: all 5 claims green, exit 0. * `core demo anchor-lens-tour`: green (no anchor-lens code touched). * `core demo orthogonality-tour`: green (5/5 claims). * Full lane: 2858 passed, 1 pre-existing failure (test_all_preamble_explains_combined_run, carried forward unchanged from main). 56 new R6 tests across three files.
17 KiB
ADR-0077 — Substantive register knobs + register-tour gate strengthening (R6)
Status: Ratified Date: 2026-05-19 Author: Shay Builds on: ADR-0068 (register pack subsystem), ADR-0070 (terse_v1), ADR-0071 (seeded variation), ADR-0072 (R5 tour), ADR-0075 (C1 coherence floor) Series: R6 — substantive register consumption (precedes T1 curated teaching depth)
Context
R1–R5 shipped the register machinery: pack class, loader, seeded variation, ChatRuntime threading, telemetry, operator tour. The orthogonality tour (ADR-0074) closed the inside-out arc with the register axis byte-orthogonal to the anchor-lens axis.
But inspection of live orthogonality-tour output reveals two real gaps:
- On
pack_grounded_surface(DEFINITION / RECALL / COMPARISON / PROCEDURE), the only knob any register currently consumes isdisclosure_domain_count, and that knob only fires on the disclosure composer — not on any pack-grounded composer. Result: across 3 × 3 × 2 = 18 cells,terse_v1is byte-identical todefault_neutral_v1on every pack-grounded surface. convivial_v1is purely decorative (seeded opening/closing markers, ADR-0071); it never adds propositional expansion.- The register-tour's
surfaces_vary_at_least_oncegate passes as long as any one register produces a different surface for any one prompt. Convivial's wrapper alone satisfies it. Result:terse_v1could remain a permanent no-op on every pack-grounded surface and CI would never flag the regression.
R6 closes both: terse becomes substantively terse, convivial gains a bounded propositional expansion, and the tour gate is replaced with a per-register substantive-distinctness claim that can no longer be satisfied by decoration alone.
Architectural constraint: trace_hash invariance under register
R5 (ADR-0072) pinned the load-bearing invariant: per-prompt trace_hash is CONSTANT across registers. The orthogonality-tour asserts this on every (lens, prompt) cell. ADR-0073d's anchor-lens arc deliberately moves trace_hash on the substantive axis; the register axis must not.
This invariant is the load-bearing reason the two axes compose without interference. R6 must preserve it.
The current implementation hashes pre_decoration_surface to
produce trace_hash. If R6 transformations modify
pre_decoration_surface, the invariant breaks. R6 therefore
introduces a new layering boundary:
composer output ──► register_canonical_surface ──► trace_hash
│
▼
R6 substantive transforms
│
▼
pre_decoration_surface
│
▼
R4 seeded marker decoration
│
▼
surface (user-facing)
register_canonical_surface is the proposition expressed neutrally
— the form the composer emits with no register touch at all.
The pipeline reads this field for trace_hash. Substantive
transformations (R6) and decorative transformations (R4) compose
after the hash is sealed.
This preserves the register-tour invariant: terse and convivial may modify the user-facing surface as much as their pack schema allows, but they can never move the trace_hash.
Decision
Schema additions
Three new register pack keys, each defaulting to "no-op" so default_neutral_v1 stays byte-identical.
terse_v1.realizer_overrides:
drop_provenance_tag : bool # default False
When True, the trailing "pack-grounded (<pack_id>)." or
"teaching-grounded (<corpus_id>): …." provenance clause is
elided from the pack-grounded composer's output.
compress_gloss : bool # default False
When True, the DEFINITION gloss is rewritten in its terse form
(drop "is a", drop leading article on the predicate noun phrase,
keep the head noun + post-modifier).
drop_articles : bool # default False
When True, surface-level "a / an / the" articles in pack-grounded
composer output are dropped where dropping them does not break
grammar (heuristic: not before a verb-after-"not" slot).
convivial_v1.realizer_overrides:
append_semantic_domain_clause : bool # default False
When True, after the canonical DEFINITION surface, append one
bounded clause expressing one additional `semantic_domain`
atom from the lemma's pack entry — deterministically selected
by lexicographic position of atoms not already realized in the
composer's gloss.
All keys are bounded, deterministic, pack-derived. No new free- form text. No model-generated expansion. No user-text influence.
default_neutral_v1 declares all three terse keys + the convivial
key explicitly as false, making the no-op contract auditable in
the pack file itself.
Composer consumption
Two consumer sites added in R6 scope:
chat/pack_grounding.py :: pack_grounded_surface(...)
+ read realizer_overrides
+ apply drop_provenance_tag, compress_gloss, drop_articles
+ apply append_semantic_domain_clause
chat/cross_pack_grounding.py :: pack_grounded_comparison_surface(...)
+ read realizer_overrides
+ apply drop_provenance_tag only (COMPARISON has no gloss/articles
surface to compress on; expansion is out of R6 scope for COMPARISON)
pack_grounded_correction_surface, pack_grounded_procedure_surface,
NARRATIVE, EXAMPLE, cross-pack chain composers accept the
realizer_overrides kwarg but consume zero new keys in R6. Their
substantive consumption is deferred to a follow-up phase keyed off
real demo evidence — same discipline as ADR-0075's deferred R1.
Layering separation
Add register_canonical_surface: str = "" to ChatResponse and
TurnEvent. The field carries the composer output BEFORE any
register transformation — substantive or decorative.
chat/runtime.py (stub path + main path):
1. composer emits canonical surface
2. register_canonical_surface = canonical
3. (NEW) substantive_surface = _apply_substantive_register(canonical, register_pack)
4. pre_decoration_surface = substantive_surface
5. decorated = decorate_surface(pre_decoration_surface, register_pack)
6. surface = decorated.surface
core/cognition/pipeline.py:
Read `register_canonical_surface` for trace_hash when present;
fall back to `pre_decoration_surface` otherwise (pre-R6 callers).
Falling back preserves byte-identity for any TurnEvent constructed
without the new field.
_apply_substantive_register is a new pure helper in
chat/register_substantive.py (sibling to chat/register_variation.py
which owns R4 decoration).
Strengthened register-tour gate
Replace (not augment) the weak surfaces_vary_at_least_once claim
in evals/register_tour/run_tour.py. The new gate:
terse_substantively_differs_from_neutral_on_at_least_one_pack_grounded_definition:
For at least one DEFINITION prompt in the tour where
grounding_source == "pack", the surface emitted under terse_v1
is byte-different from the surface emitted under
default_neutral_v1, AND that difference is not solely
whitespace/punctuation (i.e. terse has produced substantive
content compression).
convivial_substantively_differs_from_neutral_on_at_least_one_pack_grounded_definition:
Same shape applied to convivial_v1. Convivial's substantive
contribution includes the appended semantic-domain clause OR
a register marker — both are non-whitespace substantive
differences.
trace_hash_constant_across_registers_per_prompt:
Preserved verbatim from R5. The R6 layering separation makes
this invariant *load-bearing under substantive transformation*
— terse compressing the gloss must not move trace_hash.
The first two claims are the new substantive-distinctness gates. The third is R5's invariant under R6's stronger consumer set — the register-tour now asserts trace_hash invariance specifically while the surfaces are demonstrably non-trivial across registers.
surfaces_vary_at_least_once is removed. Its falsifiability hole
(convivial's wrapper alone satisfying it) is closed by construction.
Files
docs/decisions/ADR-0077-substantive-register-knobs.md NEW (this file)
packs/register/default_neutral_v1.json EDIT
- realizer_overrides explicitly sets all R6 keys to false
packs/register/terse_v1.json EDIT
- realizer_overrides: drop_provenance_tag = true
compress_gloss = true
drop_articles = true
- companion .mastery_report.json re-sealed
packs/register/convivial_v1.json EDIT
- realizer_overrides: append_semantic_domain_clause = true
- companion .mastery_report.json re-sealed
packs/register/loader.py EDIT
- widen realizer_overrides schema with the four new keys
- ratify-time validation (closed set of known keys)
scripts/ratify_register_packs.py EDIT
- gate widening: validate R6 keys; non-default values require
`register_substantively_distinct_from_neutral` invariant pass
chat/register_substantive.py NEW
- _apply_substantive_register(canonical, register_pack) -> str
- four pure transformations, each guarded by its boolean knob
chat/pack_grounding.py EDIT
- pack_grounded_surface: pass-through accepts realizer_overrides
(already in signature); consume drop_provenance_tag,
compress_gloss, drop_articles via _apply_substantive_register
- same for pack_grounded_definition path
chat/cross_pack_grounding.py EDIT
- pack_grounded_comparison_surface: consume drop_provenance_tag
chat/runtime.py EDIT
- Introduce register_canonical_surface field flow
- Insert _apply_substantive_register call between composer and
pre_decoration_surface assignment (both stub + main paths)
core/cognition/pipeline.py EDIT
- trace_hash reads register_canonical_surface when present;
falls back to pre_decoration_surface
core/physics/identity.py EDIT
- TurnEvent.register_canonical_surface: str = ""
chat/telemetry.py EDIT
- serialize_turn_event emits register_canonical_surface
(redacted under include_content=False — wire-format only)
evals/register_tour/run_tour.py EDIT
- Replace surfaces_vary_at_least_once with the three new claims
- Add terse/convivial substantive-distinctness predicates
- Preserve all_grounding_sources_identical + all_trace_hashes_identical
tests/test_register_substantive_consumption.py NEW
- Per-knob unit tests against synthetic canonical surfaces
- End-to-end: terse_v1 + DEFINITION drops provenance tag, etc.
tests/test_register_layering_separation.py NEW
- trace_hash invariance under substantive register transformation
- register_canonical_surface byte-identical across registers per prompt
tests/test_register_tour_gate_strengthening.py NEW
- The new gate catches the regression: a synthetic terse_v1 with
all R6 keys = false fails the gate; the real terse_v1 passes it.
No anchor-lens code touched. No cognition-eval surface touched. No new dynamic imports. No filesystem writes outside ratify scripts.
Invariants pinned
invariant_register_canonical_surface_constant_across_registers (NEW):
For every (lens, prompt) cell in the register-tour and
orthogonality-tour, register_canonical_surface is byte-identical
across all three registers. This is the layering-separation
proof: substantive register transformations cannot leak into the
canonical (truth-path) representation.
invariant_trace_hash_constant_across_registers_per_prompt (R5):
Preserved. Strengthened by R6: now holds while terse and
convivial produce visibly different surfaces (R6's whole point).
invariant_terse_substantively_distinct_from_neutral (NEW):
At least one pack-grounded DEFINITION prompt in the register-tour
produces a surface under terse_v1 that is byte-different from
the same prompt's surface under default_neutral_v1, with
difference not solely whitespace/punctuation.
invariant_convivial_substantively_distinct_from_neutral (NEW):
Same shape applied to convivial_v1.
invariant_realizer_no_illegal_articulation (C1):
Preserved. R6 transformations (drop_articles in particular)
must not produce surfaces that trip R2/R3. Tests pin this for
every R6 knob combination.
invariant_realizer_guard_byte_identity_on_currently_passing_cases (C1):
Preserved. Cognition eval byte-identical under default_neutral_v1
(the eval lane's pack).
Consequences
Capability unlocked
terse_v1 becomes substantively terse on DEFINITION / RECALL /
COMPARISON. convivial_v1 gains a bounded propositional expansion.
The register axis acquires real substantive consumption on the
user's most-frequented composer path.
The strengthened tour gate ensures CI catches any future regression where a register reverts to decoration-only behavior.
Cognition lane
default_neutral_v1 is the cognition-eval register. All R6 keys
default to false on neutral. Cognition eval is byte-identical
under R6 unless a future change opts neutral into a substantive
knob — and that would require its own ADR.
Performance
Three deterministic string transformations per turn under terse, one under convivial, zero under neutral. All O(surface length). No new caching.
Trust boundaries
- No new arbitrary-code surfaces. All knobs are bounded
booleans (or in
convivial's case, a single bounded clause from a pack-resident atom). Pack mutation remains proposal-only. - Closed-set schema.
register/loader.pyvalidates thatrealizer_overrideskeys belong to a known set. Unknown keys fail ratification. - Pack-derived expansion only.
convivial'sappend_semantic_domain_clausereads from the lemma's existing pack atoms. No external lookup, no LLM call, no user-text influence. - User-text mutation path unchanged. R6 transformations operate on the canonical surface, which is itself a function of pack data and the input lemma. User text never reaches the transformation logic directly.
- Telemetry redaction preserved.
register_canonical_surfaceis gated by the existinginclude_contentkwarg.
Replay determinism
Substantive transformations are pure functions of (canonical, register_pack).
Pack data is deterministic from manifest checksums. Therefore
substantive surface output is deterministic and replay-equivalent
across runs.
What this explicitly does not do
- Does not extend lens consumption beyond
pack_grounded_surface(that's the L2 anchor-lens follow-up named in ADR-0073). - Does not add new register packs. R6 is about consumption of existing knobs, not new packs.
- Does not change R4 seeded marker decoration. Convivial's openers/closers still apply post-substantive.
- Does not re-enable R1 in C1. Realizer guard stays as ADR-0075 shipped it (R2 + R3 active, R1 deferred).
- Does not touch the orthogonality-tour's anchor-lens claims. Lens engagement is unchanged; only register substantive content expands.
Verification
python -m pytest tests/test_register_substantive_consumption.py -q N passed
python -m pytest tests/test_register_layering_separation.py -q N passed
python -m pytest tests/test_register_tour_gate_strengthening.py -q N passed
core eval cognition byte-identical
under
default_neutral_v1
core demo register-tour exit 0
core demo anchor-lens-tour exit 0
core demo orthogonality-tour exit 0
Curated lanes:
smoke / cognition / teaching / packs / runtime / algebra / full
The new register-tour exit code is the canonical R6 gate. If the
tour exits non-zero while pack overrides claim non-default values,
either a register has regressed to no-op behavior or substantive
transformations have leaked into register_canonical_surface
(layering-separation breach).
Follow-ups (not in R6 scope)
- R7 — substantive knob consumption in
correction,procedure, NARRATIVE, EXAMPLE composers. Each needs its own composer-specific knob design; bundle is too large for one ADR. - R8 — register-aware lens annotation rendering. Currently
[lens(<id>):<mode>]is a fixed presentation; terse might prefer a compressed sigil, convivial a more expressive phrasing. Out of R6 scope to keep the lens-axis stable while the register axis expands. - T1 — curated teaching depth (the next item in the sequence). Independent of R6; can ship in parallel once C2 lands.
- R6.1 — additional knob shapes (e.g.,
terse_v1gaining a bullet-form output knob for COMPARISON). Deferred until the current R6 knobs have lived for at least one full eval cycle.