9.3 KiB
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:
- The unified type stays in
epiphany_core::accidental;epiphany-layout-irimportsepiphany_core::SmuflVersion(already re-exported at core's root,lib.rs:128). Do not move the type between core modules. - 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 mutatesmetrics_hash[0] ^= 1(resolved.rs:477) and never touchessmufl_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::LayoutCacheis 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— deletepub struct SmuflVersion { major, minor }.- Re-export or
use epiphany_core::SmuflVersionso 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 inepiphany-core/src/accidental.rs:251-258— the homonym is gone. :90—GlyphCatalogIdentity.smufl_versionnow takes core's type.:120(Default),:332(BravuraCatalog::smufl_version),:519(the test catalog impl) —SmuflVersion { major: 1, minor: 4 }becomesSmuflVersion::from_decimal(1, "4").expect("1.4 is a valid SMuFL version"), i.e.minor_centi: 40. Usefrom_decimal, not a hand-writtenminor_centi: 40— the field's doc forbids literal construction precisely because4and40look interchangeable and are not.:310— theGlyphCatalog::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, nodecode_vectors.txt.epiphany-core's freshly frozen major-3 layout is off-limits except for deleting the stale homonym paragraph inaccidental.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 --checkcargo clippy --workspace --all-targets→ 0 warningscargo test --workspace→ 0 failed (report the pass count; 3b-i landed at 1282)RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps→ 0cargo run -q -p epiphany-testkit --example conformance_suite→ 8/8cargo 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 defaultGlyphCatalogIdentitybefore 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 statusovergoldens/,tests/golden/,spec/vectors/). epiphany_layout_ir::SmuflVersionstill resolves for downstream crates.- Requirement counts unmoved, and no
\label{req:added to the spec diff.