# CONTRACT — the core decode-vector surface **Status:** dispatch-ready. Delivers the future revision the Binary Format companion already prescribes (`binary_format.tex:3294-3296`) and closes the gap found empirically in Push 4b tranche 3b-i. **Ratified by the user 2026-07-23:** 1. **Coverage** — the spec's five representative layouts, **plus** the schema-major-3 tuning-context types, **plus** one whole-`Score` vector at each schema major 0/1/2/3. 2. **Strictness** — `check()` calls **genuine public core API**, not a harness-side wrapper, so a leaf reject vector exercises production code. --- ## Why this exists `spec/vectors/decode_vectors.txt` is normative under `req:binfmt:decode-vectors`: it is what a *second implementation* is checked against. Today it holds 65 vectors across 5 surfaces (`ops.operation_kind_tag`, `ops.materialized_state`, `bundle.block`, `bundle.manifest`, `bundle.operation_index`) and **nothing from `epiphany-core`** — the score wire, the oldest and most load-bearing format in the repo, has no cross-implementation vectors at all. The companion already asked for this (`binary_format.tex:3294`): "A future revision should extend the corpus to the representative struct layouts of Section~\ref{sec:values:representative}, which remain **round-trip locked rather than literal-byte locked**." That is not theoretical. In tranche 3b-i the reviewer swapped `smufl` and `overrides` in **both** halves of `ScoreTuningContext`'s codec — a self-consistent reordering of a layout `req:binfmt:frozen-layout` freezes permanently — and **the entire workspace suite (1283 tests) and all 8/8 conformance rows passed.** Round-trip locking cannot see that. Only literal bytes can. ## The mechanism already exists — do not invent a new one - **`CanonicalValue`** (`codec.rs:3435`) is **public** and already implements exactly the required discipline: decode, `finish()` (reject trailing bytes), re-encode, and reject on mismatch. It already covers **four of the spec's five** representative layouts — `Pitch`, `TimeAnchor`, `Event`, `Slur`. - `Codec` and `Reader` are `pub(crate)` and **MUST stay that way.** A module inside `epiphany-core` can use them directly; nothing new goes public except the `CanonicalValue` impls below and the two `vectors` functions. - `encode_v0_score` / `encode_v1_score` / `encode_v2_score` are `pub(crate)` — an in-core `vectors` module can call them to synthesize genuine old-major bytes. ## 1. Extend `CanonicalValue` Add to the `canonical_value!` list (`codec.rs:3476`), each because the corpus pins it: - **`RationalTime`** — the fifth representative layout, and the only one missing. It **normalizes on decode** (an unreduced fraction reduces to lowest terms, `codec.rs:2570`), which makes it the exemplar of the spec's own warning that "a guard on an outer value can *mask* a lenient inner codec". - **`ScoreTuningContext`** — the schema-major-3 container whose silent reorderability is the reason this tranche exists. - **`TuningOverride`**, **`TuningScope`**, **`SmuflVersionRequirement`**, **`SmuflVersion`** — the four leaf layouts frozen in 3b-i. Update the macro's doc rationale (`codec.rs:3433`, "Implemented only for the value types operation payloads embed... to keep the public surface intentional") to record the second reason: **and the types the decode-vector corpus pins**. The surface stays intentional; the intent widened. If any of these lacks a `Codec` impl, stop and report rather than inventing a byte layout — every one of them is already encoded inside `Score`, so the layout exists and `CanonicalValue` must introduce **no new bytes** (that is the trait's stated contract). ## 2. New `crates/epiphany-core/src/vectors.rs` Mirror `epiphany-ops/src/vectors.rs` in shape and doc style: ```rust pub type DecodeVector = (&'static str, &'static str, &'static str, String, Vec); pub fn decode_vectors() -> Vec; pub fn check(surface: &str, bytes: &[u8]) -> Option>; ``` `check` semantics, exactly as the other two crates: `Err` = rejected, `Ok(injective)` = accepted with whether it re-encodes to its own bytes. Route every leaf surface through **`CanonicalValue::decode_canonical`** — the public production API — never a locally re-implemented strictness check. ### Surfaces Leaves (one per `CanonicalValue` type above): `core.rational_time`, `core.time_anchor`, `core.pitch`, `core.event`, `core.slur`, `core.score_tuning_context`, `core.tuning_override`, `core.tuning_scope`, `core.smufl_version_requirement`, `core.smufl_version`. Whole `Score`, one per schema major: `core.score_v0`, `core.score_v1`, `core.score_v2`, `core.score_v3`. **Every surface MUST carry both an `accept` and a `reject` vector** — `every_surface_carries_both_verdicts` enforces it. If a surface cannot be given a meaningful reject vector, fold it into its container rather than inventing a contrived one, and say so in your report. ### The trap in the per-major Score vectors For `core.score_v3`, injectivity is `decoded.canonical_bytes() == bytes`. For `core.score_v0/v1/v2` it is **NOT**. Migration deliberately produces a value whose *current* encoding differs from the input bytes — that is the whole point of a default-filling migration. `decode_vN_score` is already strictly canonical **over its own wire form** (it re-encodes through `encode_vN_score` and rejects a mismatch), so a successful `Score::decode_canonical_versioned(bytes, N)` already proves the input was canonical at major N. Return `Ok(true)` there. **Do not compare against `canonical_bytes()` for majors 0–2** — it will fail, and "fixing" it by relaxing the vector would destroy the vector's meaning. ### Reject classes — principled, not arbitrary The ops module's rule applies (`vectors.rs:10`): each class is one this repository actually shipped a bug in, or one an injectivity fuzzer cannot see. Draw from: - **unreduced `RationalTime`** — the lenient-leaf normalization case; - **non-finite float** (NaN / ±inf bits) in a `CanonicalF64` leaf (`NonFiniteFloat`); - **unsorted or duplicated** `BTreeSet`/`BTreeMap` entries (order-preserving sequence checks are invisible to a whole-value re-encode guard); - **out-of-range discriminant** on a tagged union (`TimeAnchor`, `TuningScope`, `PitchSpacePosition`); - **trailing bytes** and **truncation**; - **the swapped major-3 field order** — bytes encoding `ScoreTuningContext` with `overrides` before `smufl`. This is the direct regression vector for the 3b-i defect and MUST be present. ## 3. Wire it into the testkit `crates/epiphany-testkit/src/vectors.rs`: - `all()` (:78) — chain `epiphany_core::vectors::decode_vectors()`. - the `check` dispatch (:165) — add `.or_else(|| epiphany_core::vectors::check(..))`. **Append core LAST**, after ops and bundle. This is deliberate: it keeps the regenerated corpus diff **purely additive**, which *proves* no existing vector's bytes moved — i.e. that this tranche changed no existing wire form. A nicer-looking dependency order would move all 65 existing lines and destroy that property. Update `all()`'s doc comment ("the operation layer, then the bundle wire") accordingly. ## 4. Regenerate `cargo run -q -p epiphany-testkit --example generate_vectors` `the_committed_corpus_matches_the_generator` fails on drift, so the regenerated file must be committed. Confirm in your report that **the first 65 vectors are byte-identical** to before. ## 5. Spec Update the rationale at `binary_format.tex:3294-3296`: the "future revision" it anticipates is now delivered for the representative layouts, and record that the corpus additionally pins the schema-major-3 tuning-context layouts and the per-major whole-`Score` forms. If §"The Decode Vector Corpus" enumerates surfaces, extend that list. **No new `\label{req:...}`** — `req:binfmt:decode-vectors` already governs this and needs no change. Requirement counts MUST stay **212 / 282 / 282**. ## Do NOT - Make `Codec` or `Reader` public. - Introduce any new byte layout. `CanonicalValue` must emit exactly the bytes the whole-score codec already emits (its stated contract). If a value's per-type bytes differ from its embedded bytes, that is a bug — stop and report it. - Change any existing vector, or any wire form. - Touch the editor track (`spec/PLAN_EDITOR_APP.md`, `CONTRACT_EDITOR_*`, `crates/epiphany-editor-gui/goldens/`) or `.claude/worktrees/`. ## The gate (report exact numbers) - `cargo fmt --all --check` - `cargo clippy --workspace --all-targets` → 0 warnings - `cargo test --workspace` → 0 failed (HEAD is at 1318; report the count) - `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` → 0 - `cargo run -q -p epiphany-testkit --example conformance_suite` → 8/8, and report the vector count gate [7d] prints (65 today) - `cargo test -p epiphany-testkit --test requirement_labels` → 6/6, counts **212 / 282 / 282** ## Prove the hole is closed — the deliverable that matters A corpus that does not catch the original defect is decoration. **Re-apply the 3b-i mutation** — swap `smufl` and `overrides` in *both* the `enc` and `dec` halves of `impl Codec for ScoreTuningContext` (`codec.rs:1960`) — and confirm the **corpus check now fails** (`every_vector_gets_its_declared_verdict` / `the_committed_corpus_matches_the_generator`). Restore, and assert the anchor text is back. Report the exact failure message. That mutation previously passed 1283 tests and 8/8 conformance. If it still passes after this tranche, the tranche did not do its job. ## What the reviewer will verify independently - The first 65 corpus vectors are byte-identical; the diff is purely additive. - The 3b-i swap mutation is caught by the corpus. - The per-major `Score` vectors genuinely decode at their stamped major, and `core.score_v0/v1/v2` are *not* asserting `canonical_bytes()` equality. - Every new `accept` vector's bytes equal the bytes the whole-score codec embeds for that value (no new layout). - No new public API beyond the `CanonicalValue` impls and the two `vectors` functions; `Codec`/`Reader` still `pub(crate)`.