core/docs/decisions/ADR-0077-substantive-register-knobs.md
Shay 5a78b0e37b feat(register): ADR-0077 — substantive register knobs + layering boundary (R6)
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.
2026-05-19 23:39:11 -07:00

424 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-0077 — Substantive register knobs + register-tour gate strengthening (R6)
**Status:** Ratified
**Date:** 2026-05-19
**Author:** Shay
**Builds on:** [ADR-0068](./ADR-0068-register-pack-subsystem.md) (register
pack subsystem), [ADR-0070](./ADR-0070-register-terse-v1.md) (terse_v1),
[ADR-0071](./ADR-0071-seeded-variation.md) (seeded variation),
[ADR-0072](./ADR-0072-register-telemetry-operator-surface.md) (R5 tour),
[ADR-0075](./ADR-0075-realizer-slot-type-guard.md) (C1 coherence floor)
**Series:** R6 — substantive register consumption (precedes T1 curated
teaching depth)
---
## Context
R1R5 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** is
`disclosure_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_v1` is byte-identical
to `default_neutral_v1` on every pack-grounded surface.
* `convivial_v1` is purely decorative (seeded opening/closing
markers, ADR-0071); it never adds propositional expansion.
* The register-tour's `surfaces_vary_at_least_once` gate passes as
long as **any one** register produces a different surface for
any one prompt. Convivial's wrapper alone satisfies it.
Result: `terse_v1` could 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.py` validates that
`realizer_overrides` keys belong to a known set. Unknown keys
fail ratification.
* **Pack-derived expansion only.** `convivial`'s
`append_semantic_domain_clause` reads 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_surface`
is gated by the existing `include_content` kwarg.
### 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_v1` gaining a
bullet-form output knob for COMPARISON). Deferred until the
current R6 knobs have lived for at least one full eval cycle.