Contract the G-minor vocabulary audit: a matrix, read-only

The gating work before G-minor can be contracted. Deliverable is one file,
spec/AUDIT_GMINOR_VOCABULARIES.md, holding a reachability matrix keyed
chunk role -> encoded payload type -> discriminant vocabulary ->
post-baseline variants -> introduction event -> derivation site.

Method is pinned to walking the encoder rather than the type, because a field
can exist and never reach the wire -- ScoreTuningContext::accidental_extensions
is the standing proof -- and the matrix's purpose is to say what is emitted.

Inclusions cover nested additive variants at any depth and, the sharpest edge,
later native additions to enums that also carry a Registered escape: the escape
is out of scope while a new native variant on the same enum is not, and
binary_format names fifteen such carriers. ChunkKind is excluded as closed.

The trap most likely to corrupt the matrix: the spec's "append at >= N"
phrasing names the next free slot, not the baseline boundary. OperationPayload
is documented as appending at >= 4 while discriminant 3 is itself an append,
and OperationKind at >= 30 while 24-29 were appends too. Post-baseline
membership must come from revision history, not from those numbers.

Epoch numbers are explicitly not ratified here; column 5 records the
introduction event. Completeness is enforced by requiring a per-role
accounting including roles that produce no rows, and negative results recorded
for every escape-carrying enum -- an unlisted role being indistinguishable
from an overlooked one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
Levi Neuwirth 2026-07-28 18:02:32 -04:00
parent 82ac636982
commit 6145efecd9
1 changed files with 169 additions and 0 deletions

View File

@ -0,0 +1,169 @@
# Contract: G-minor vocabulary audit — a reachability matrix, and nothing else
Repo root `/home/jeans/Repos/active/epiphany`. Governed by
`spec/PLAN_GMINOR_SCHEMA_MINOR.md` (§3 and §5.1; policy ratified 2026-07-28)
and `spec/PASS13_CANDIDATES.md` (P13-S14).
**This is a READ-ONLY packet.** It produces one document. It changes no Rust,
no `.tex`, no vectors, and no goldens.
**Parallel safety.** The editor track owns `crates/epiphany-editor-gui/**`,
`crates/epiphany-render-svg/**`, `crates/epiphany-glyphs/**`,
`spec/PLAN_EDITOR_APP.md`, every `spec/CONTRACT_EDITOR_*.md`,
`spec/DRAFT_T4_FIXTURE_RECIPE.md`, the entire `spikes/` tree, and the current
unstaged root `Cargo.toml` change. **All of it is out of scope**, and since
this packet writes only one new file, none of it should ever be staged.
---
## Why this packet exists
The G-minor rung cannot be contracted until every append-only discriminant
reachable from an affected chunk payload is known. A first attempt scoped the
problem as per-`OperationKind` and recommended a policy that could not work —
an envelope also emits the outer `OperationPayload` discriminant, and
`ResolveEquivocation` carries no `OperationKind` at all. **That scoping error
is the reason this audit is a separate packet rather than a paragraph in a
contract.**
## The deliverable
One new file, `spec/AUDIT_GMINOR_VOCABULARIES.md`, whose core is a
**reachability matrix** with exactly these columns:
| chunk role | encoded payload type | discriminant vocabulary | post-baseline variants | introduction event | derivation site |
* **chunk role** — a `ChunkKind` variant (`bundle/src/chunk.rs:18`; nine of
them). Every role gets at least one row or an explicit "emits no additive
discriminant" row with the reason.
* **encoded payload type** — the Rust type whose canonical encoding *is* that
role's payload. Some roles are payload-polymorphic (`Snapshot` is the
canonical-base `MaterializedState` *or* the acceleration full-`Score`); those
get one row per form, distinguished.
* **discriminant vocabulary** — the enum whose tag is written.
* **post-baseline variants** — the specific variants appended after the
format's initial ratification, listed individually with their discriminant
values. Not a count.
* **introduction event** — the tranche or revision that appended it (e.g.
"Phase-3 first tranche", "schema-major-2 repeat revision", "Push 4a",
"genesis G1", "genesis G2a"). **Name the event, not an epoch number.**
* **derivation site**`file.rs:line` of the code that writes the tag, so the
G-minor implementation knows where `introduced_minor` has to live.
## Method — walk the encoder, never the type
**The ground truth is the encoder, not the struct definition.** A field can
exist on a type and never reach the wire: `ScoreTuningContext::accidental_extensions`
is staged out of schema major 3 and its `Codec` deliberately drops it
(`core/src/codec.rs:1939`). A type-driven walk would record a vocabulary that
is not actually emitted, and the matrix's whole purpose is to say what is
*emitted*.
So: start from each role's payload type, walk its `encode_canonical` /
`Codec::enc` implementation transitively, and record every site that writes a
tag or discriminant. `grep -rn "push_tag\|fn discriminant"` over
`crates/epiphany-core/src` and `crates/epiphany-ops/src` returns ~65 hits
across 11 files and is a reasonable starting net — but it is a *net*, not the
method. Confirm reachability from a role by following the encoder.
## Inclusions and exclusions
**IN scope:**
* Native variants appended to any open vocabulary after baseline.
* **Nested** additive variants — a vocabulary reached only through another
value's encoder counts, at whatever depth.
* Later native additions to enums that *also* carry a `Registered` escape.
**This is the audit's sharpest edge**: the escape variant is out of scope,
but a new native variant on the same enum is in scope, and the two are easy
to conflate. `binary_format.tex` names fifteen escape carriers
(`RepairKind`, `ReanchorReason`, `PreconditionFailureReason`,
`IntegrityAnomalyKind`, `ReplicaAnomalyReason`, `TransactionCategory`,
`ResolutionAction`, `ConflictKind` via `ExtensionConflict`,
`BarrierScope`/`BarrierCondition`, `TieClass`, `StaffGroupKind`,
`PitchSpacePosition`, `SpellingNominal`, `TypedObjectId`, and the barrier
`ObjectKind`); every one needs checking for native appends.
**OUT of scope:**
* **`Registered` payload values.** Extension through an escape variant is not a
schema change at all — the wire form is already defined.
* **`ChunkKind` itself.** It is closed: no `Registered` variant, and its
discriminant enters every chunk's hash preimage, so a new chunk kind is a
format-**major** event.
* Anything requiring a schema-major bump. Field additions are major
*regardless of type* — an `Option` still occupies a positional slot
(`binary_format.tex:2360`). Only discriminant appends are minor-additive.
## Traps
1. **The spec's "append at ≥N" phrasing names the next free slot, not the
baseline boundary.** `binary_format.tex:2369` says `OperationPayload`
appends at ≥4 — yet discriminant **3** (`ResolveEquivocation`) is itself an
append, as both the spec (`:1291`) and the code comment
(`payload.rs:80`, "the ratified 0..=2 stay stable") state. Likewise
`OperationKind` "appends at ≥30" while 2429 were also appends. **Do not
derive post-baseline membership from the ≥N numbers.** Establish each
variant's introduction event from the spec's revision history, the
`DECISIONS.md` files, and `git log` — and say which source settled it when
they disagree.
2. **`OperationKind` and `OperationKindTag` are independent, misaligned
spaces.** `RespellPitch` is kind 2 / tag 3. They are two rows, never one.
3. **Payload-polymorphic roles.** `Snapshot` carries both the canonical-base
`MaterializedState` and the acceleration full-`Score`; they emit different
vocabularies and must not be merged. The canonical base notably does **not**
emit `OperationKind` at all.
4. **A hand-written `discriminant()` match is not evidence of completeness.**
`OperationKind::discriminant()` is exactly the site Push 4a got wrong. Where
a macro-generated vocabulary exists (`operation_kind_tag_vocabulary!`),
prefer it as the source of truth and note the divergence if the hand-written
sibling disagrees.
## What this packet must NOT do
* **Do not ratify epoch numbers.** Column 5 records the introduction *event*.
Assigning epochs is a later ruling that depends on this matrix being
complete.
* **Do not write or modify any Rust**, including tests, and do not add
`introduced_minor` anywhere.
* **Do not modify any `.tex`**, vectors, or goldens.
* **Do not draft the G-minor implementation contract.**
* Do not touch `spikes/`, the root `Cargo.toml`, or any editor-track file.
## Completeness — how to know the matrix is done
The audit's failure mode is a missed vocabulary, and it is silent. So the
report must state, for each of the nine roles, either the rows it produced or
why it produces none — an unlisted role is indistinguishable from an
overlooked one.
State explicitly:
* which roles emit no additive discriminant, and why;
* every escape-carrying enum checked for native appends, **including those that
turned out to have none** — a negative result recorded is evidence, a
negative result omitted is a gap;
* any variant whose introduction event could not be established from the
sources in trap 1, flagged as **unresolved** rather than guessed;
* any place where the spec and the code disagree about what was appended when.
**A guess presented as a finding is the worst possible output here**, because
the epoch mapping will be ratified on top of it and a wrong epoch is a wrong
wire record that then freezes.
## Gate
`cargo fmt --check` and `cargo test --workspace` are expected to be **untouched
and green** — that is the check that this packet really was read-only. Report
`git status --short` verbatim so the file list can be seen to contain exactly
one new file.
## Report
The matrix itself, plus: the completeness statement above; the sources used to
establish introduction events and which settled disputes; anything found that
the plan's §3 four-vocabulary table did not name; and any place where this
contract's own assumptions turned out to be wrong. That last one is not
politeness — the plan this packet serves already had to be revised once for a
scoping error of exactly this kind, and the contract's four-vocabulary starting
table is a floor, not a ceiling.