# CONTRACT — Push 4b tranche 3b-ii: the SmuflVersion unification and the GlyphCatalogIdentity move **Status:** dispatch-ready. Closes P13-S12. Follows tranche 3b-i (68b08ad), which opened schema major 3. **Ratified by the user 2026-07-23:** 1. The unified type **stays in `epiphany_core::accidental`**; `epiphany-layout-ir` imports `epiphany_core::SmuflVersion` (already re-exported at core's root, `lib.rs:128`). Do **not** move the type between core modules. 2. The core-spec definition is a **type listing with normative prose, no new `\label`**. Requirement counts MUST stay **212 / 282 / 282**. --- ## What this tranche does `epiphany-layout-ir` has its own `SmuflVersion { major: u16, minor: u16 }` (`glyph.rs:29`) storing the **literal** minor digit — `{1, 4}` for SMuFL 1.4. Derived `Ord` on a literal minor orders 1.3 and 1.4 **before** 1.12, which is backwards versus SMuFL's real release history (1.12 → 1.18 → 1.20 → 1.3 → 1.4). **That bug is live in layout-ir today**, and the type is a direct field of `GlyphCatalogIdentity` — layout-conformance identity. Tranche 3a defined the correct type in core (`minor_centi`, fraction-normalized to hundredths) and deliberately left layout-ir's alone as a bounded homonym. This tranche **unifies them**: layout-ir deletes its own and uses core's. The crate graph forces the direction — `layout-ir` depends on `core` (`Cargo.toml:12`); core depends only on `determinism`. Core's type is necessarily the survivor. ## Blast radius: NO golden and NO vector moves. Do not go looking for any. The reviewer verified this before dispatch, and it **corrects the P13-S12 ledger entry**, which says the move lands "with golden/vector regen": - Every assertion on `ResolvedLayoutIR::canonical_bytes()` is **relative**: stability (`resolved.rs:420`), determinism (`reference_suite.rs:228-230`), and sensitivity — which mutates `metrics_hash[0] ^= 1` (`resolved.rs:477`) and never touches `smufl_version`. **No absolute hash or byte literal is pinned anywhere in the workspace.** - The committed SVG goldens (`epiphany-render-svg/tests/golden/`) and PNG baselines (`epiphany-editor-gui/goldens/`) do **not** embed the catalog identity. Their only "SMuFL" is a fixed comment string in the SVG preamble. **Conformance gate [9] and the editor track are untouched.** - `ChunkKind::LayoutCache` is a regenerable major-0 role — discard-and-regenerate. No migration, no schema major, **no wire change of any kind in this tranche.** The encoded catalog bytes do change in *value* (`encode_catalog` writes the minor as `04 00` today and `28 00` after), and that is expected and unpinned. If a golden or a byte-literal assertion *does* fail, **stop and report it** — it would mean the reviewer's analysis missed a pin, and it must not be "fixed" by re-copying bytes. ## The surface **`crates/epiphany-layout-ir/src/glyph.rs`** - `:29` — delete `pub struct SmuflVersion { major, minor }`. - Re-export or `use epiphany_core::SmuflVersion` so the name still resolves in-crate. Keep the doc note at `:27` (Chapter 7/9 role) attached to the re-export, and drop the now-stale "This is not epiphany_layout_ir::SmuflVersion" paragraph in `epiphany-core/src/accidental.rs:251-258` — the homonym is gone. - `:90` — `GlyphCatalogIdentity.smufl_version` now takes core's type. - `:120` (`Default`), `:332` (`BravuraCatalog::smufl_version`), `:519` (the test catalog impl) — `SmuflVersion { major: 1, minor: 4 }` becomes `SmuflVersion::from_decimal(1, "4").expect("1.4 is a valid SMuFL version")`, i.e. `minor_centi: 40`. Use `from_decimal`, not a hand-written `minor_centi: 40` — the field's doc forbids literal construction precisely because `4` and `40` look interchangeable and are not. - `:310` — the `GlyphCatalog::smufl_version()` trait method return type. **`crates/epiphany-layout-ir/src/resolved.rs:364`** — `c.smufl_version.minor` → `.minor_centi`. This is the one line that changes emitted bytes. **`crates/epiphany-layout-ir/src/solver.rs:769`** — `smufl_version.minor += 1` → `.minor_centi += 1`, inside `forged_catalog_metadata_is_rejected`. Intent is "a catalog identity that does not match the bundled Bravura is rejected"; `+= 1` still denotes a different (nonsense) version, which is the point. Preserve it. **`crates/epiphany-layout-ir/src/lib.rs:113`** — **keep** `SmuflVersion` in the public re-export list, now aliasing core's type. This keeps `epiphany_layout_ir::SmuflVersion` compiling for every downstream user, so downstream churn is zero. **`crates/epiphany-testkit/src/layout_stub.rs:798`** — `gen_smufl_version` currently emits `minor: rng.range(0, 6)`, which as *hundredths* would mean versions 1.00–1.05 — not real SMuFL versions. Generate meaningful normalized values instead (e.g. sample from the real release set {12, 18, 20, 30, 40}, or scale a small digit by 10). Say in a comment why. **A `const` contingency:** `from_decimal` returns `Option` and is not `const`. Every current site is a function body, so this does not arise. If you find a site that genuinely needs a `const`, add a `const fn` constructor to `epiphany_core::accidental::SmuflVersion` rather than hand-writing `minor_centi: 40` at the call site — and say so in your report. ## Spec — `spec/core_spec.tex` `SmuflVersion` is currently **defined nowhere in core_spec.tex**: only `SmuflVersionRequirement` (`:3269`, whose two fields are of that type) and two Chapter 9 usages — `GlyphCatalog::smufl_version()` (`:10420`) and `GlyphCatalogIdentity.smufl_version` (`:10460`). P13-S12's ratified shape reached Rust and the Binary Format companion but never the core specification. This tranche closes that — it is the last open half of S12. Add **one** definition (near `SmuflVersionRequirement` at `:3269`, the Chapter 4 site that first needs it) as a type listing: ``` pub struct SmuflVersion { pub major: u16, /// The fractional part, normalized to hundredths. pub minor_centi: u16, } ``` with normative prose stating: the minor is stored **fraction-normalized to hundredths** — 1.12 → 12, 1.18 → 18, 1.20 → 20, 1.3 → 30, 1.4 → 40 — so that the derived ordering on `(major, minor_centi)` matches SMuFL's real release order; that a literal-digit encoding would order 1.3 and 1.4 before 1.12 and is non-conforming; and that the normalization collapses the 1.2/1.20 ambiguity (both → 20). Include the release table as an explanatory note, not as normative text. Make the two Chapter 9 sites reference this single definition (a cross-reference sentence is enough) so it is unambiguous that the glyph catalog's version and the tuning context's version are **the same type**. **No new `\label{req:...}`.** The counts MUST remain 212/282/282 — the gate asserts it. The wire-level rule is already normative in `binary_format.tex:2963-2967`; this is the core-side type definition, not a new requirement. ## Ledger — `spec/PASS13_CANDIDATES.md` P13-S12's resolution text says the unification lands "with golden/vector regen." Verified false before dispatch (see *Blast radius*). Correct that clause to state that no golden or vector is pinned to the catalog identity, so the move changes emitted bytes with nothing to regenerate. This is the S9 discipline the ledger already applies to itself — a claim that reads cleanly but is not supported. ## Do NOT touch - **Any wire format.** 3b-ii changes no schema major, no `Codec`, no `decode_vectors.txt`. `epiphany-core`'s freshly frozen major-3 layout is off-limits except for deleting the stale homonym paragraph in `accidental.rs`. - **The editor track**: `crates/epiphany-editor-gui/goldens/*.png`, `spec/PLAN_EDITOR_APP.md`, `spec/CONTRACT_EDITOR_T1A_GOLDENS.md`, `spec/CONTRACT_EDITOR_T2_SELECTION.md`. Parallel work; not yours. - The committed SVG goldens under `crates/epiphany-render-svg/tests/golden/`. - `.claude/worktrees/` — agent worktrees, not the repo. ## The gate (report exact numbers) - `cargo fmt --all --check` - `cargo clippy --workspace --all-targets` → 0 warnings - `cargo test --workspace` → 0 failed (report the pass count; 3b-i landed at 1282) - `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` → 0 - `cargo run -q -p epiphany-testkit --example conformance_suite` → 8/8 - `cargo test -p epiphany-testkit --test requirement_labels` → 6/6, counts **212 / 282 / 282** unchanged ## Deliver a test that would have caught the live bug The whole point of this tranche is that layout-ir's ordering was wrong. Add a test **in layout-ir** proving the catalog's version type now orders the real release sequence correctly (1.12 < 1.18 < 1.20 < 1.3 < 1.4) — the assertion that fails against the deleted literal-minor type. A unification that silently fixed a bug with no test naming it is not finished. ## What the reviewer will verify independently — build to survive it - The emitted catalog bytes changed at exactly the expected offset (`04 00` → `28 00`) and nowhere else, by encoding a default `GlyphCatalogIdentity` before and after. - The ordering bug is genuinely dead: the new test fails if the comparison is performed on literal minors. - No committed golden, baseline, or vector file changed (`git status` over `goldens/`, `tests/golden/`, `spec/vectors/`). - `epiphany_layout_ir::SmuflVersion` still resolves for downstream crates. - Requirement counts unmoved, and no `\label{req:` added to the spec diff.