diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index c7526b3..a3e0f03 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -1022,10 +1022,22 @@ until the shapes have had a consumer. sort before 1.12) would not lock S12 at all; see the mutation below, which reproduces exactly that failure and confirms this test catches it. **Not** `epiphany_layout_ir::SmuflVersion` (`glyph.rs:29`, literal-minor, - load-bearing for `GlyphCatalogIdentity`) — that type is untouched; the two - are a deliberate, bounded homonym (`epiphany-core` cannot depend on - `epiphany-layout-ir` in any case) until a later tranche unifies them and - moves `GlyphCatalogIdentity`, with golden regen. + load-bearing for `GlyphCatalogIdentity`) — at this tranche that type was + untouched, the two a deliberate, bounded homonym (`epiphany-core` cannot + depend on `epiphany-layout-ir` in any case), pending a later unification. + + **Superseded by tranche 3b-ii (2026-07-23), which corrects two forward-looking + claims made here.** The homonym is gone: layout-ir's type is deleted and + re-exports this one, so `GlyphCatalogIdentity` now carries the normalized + shape and its backwards ordering (live at the time this was written) is + fixed. And the anticipated **"golden regen" never happened — there was + nothing to regenerate.** No golden, baseline, or vector anywhere in the + workspace is pinned to the catalog identity: every assertion on + `ResolvedLayoutIR::canonical_bytes()` is *relative* (stability, determinism, + and a sensitivity check that mutates `metrics_hash`, never `smufl_version`), + and the committed SVG/PNG goldens do not embed it. The encoded minor moved + `0x04` → `0x28` with no committed bytes pinning it. See + `epiphany-layout-ir/DECISIONS.md` for the unification itself. Also new: `CustomGlyphId`, `ModificationRegistryId`, `AccidentalGroupId` (`catalog_id!` entries in `pitch.rs`, beside `AccidentalRegistryId`/ diff --git a/crates/epiphany-core/src/accidental.rs b/crates/epiphany-core/src/accidental.rs index 40c69c5..d161f2e 100644 --- a/crates/epiphany-core/src/accidental.rs +++ b/crates/epiphany-core/src/accidental.rs @@ -35,11 +35,9 @@ //! pinned frame — see its doc comment. //! * **S12** — [`SmuflVersion`] stores its minor fraction-normalized to //! hundredths (`minor_centi`), not literally, so derived `Ord` agrees with -//! SMuFL's real release order. **This is not** -//! `epiphany_layout_ir`'s existing, differently-shaped `SmuflVersion` -//! (`glyph.rs:29`, literal-minor) — see this type's doc comment for why the -//! two are a deliberate, bounded homonym until Push 4b tranche 3b unifies -//! them. +//! SMuFL's real release order. `epiphany_layout_ir` defined its own, +//! differently-shaped, literal-minor `SmuflVersion` until Push 4b tranche +//! 3b-ii unified the two onto this type — see this type's doc comment. use core::num::NonZeroU32; @@ -248,14 +246,10 @@ pub struct ScoreAccidentalExtensions { /// otherwise look interchangeable and are not: the former is 1.3, the latter /// is nonsensical). /// -/// **This is not** `epiphany_layout_ir::SmuflVersion` (`glyph.rs:29`, -/// `{ major: u16, minor: u16 }`, literal-minor, load-bearing for -/// `GlyphCatalogIdentity`). That type is Chapter 7's own and stays untouched: -/// unifying the two, and moving `GlyphCatalogIdentity` onto the normalized -/// shape, is Push 4b tranche 3b's job, done deliberately with golden regen. -/// `epiphany-core` cannot depend on `epiphany-layout-ir` in any case, so -/// within this crate there is no ambiguity — the two are a deliberate, -/// bounded homonym until then. +/// This is also `epiphany_layout_ir::SmuflVersion`: Push 4b tranche 3b-ii +/// (P13-S12) unified layout-ir's former literal-minor homonym onto this type +/// (`glyph.rs` re-exports it), so the glyph catalog's version and the tuning +/// context's version are the same type. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct SmuflVersion { pub major: u16, diff --git a/crates/epiphany-layout-ir/DECISIONS.md b/crates/epiphany-layout-ir/DECISIONS.md index 4cee811..375fb4d 100644 --- a/crates/epiphany-layout-ir/DECISIONS.md +++ b/crates/epiphany-layout-ir/DECISIONS.md @@ -874,3 +874,59 @@ end, where it tucked a full notehead width to the *left* of the final note. **Deferred (Standard tier):** breaking a slur that clearance would make absurdly tall, per-kind appearance (`SlurKind::Phrase` etc.), and shaping against ledger lines and accidentals rather than notehead boxes. + +## `SmuflVersion` unification: layout-ir's literal-minor homonym is deleted (Push 4b tranche 3b-ii, P13-S12, 2026-07-23) + +`epiphany-layout-ir` had its own `SmuflVersion { major: u16, minor: u16 }` +(`glyph.rs:29`), storing the minor **literally**. Derived `Ord` therefore +sorted 1.3 and 1.4 **before** 1.12 — backwards versus SMuFL's real release +order (1.12 → 1.18 → 1.20 → 1.3 → 1.4) — and the type was a direct field of +`GlyphCatalogIdentity`, layout-conformance identity. **That bug was live.** + +Tranche 3a (P13-S10/S11/S12, ratified together) defined the correct +fraction-normalized type in `epiphany_core::accidental::SmuflVersion` +(`minor_centi`, e.g. `from_decimal(1, "4")` → `40`) for the tuning-context +use, deliberately leaving layout-ir's alone as a bounded homonym — core +cannot depend on layout-ir, so there was no way to share the type from that +side. This tranche closes the other half: layout-ir deletes its own type and +re-exports core's (`glyph.rs`: `pub use epiphany_core::SmuflVersion;`), so +`epiphany_layout_ir::SmuflVersion` keeps resolving for every downstream user +with zero call-site churn beyond the three literal-construction sites +(`GlyphCatalogIdentity::default`, `BravuraCatalog::smufl_version`, and the +in-crate test catalog), which now go through `SmuflVersion::from_decimal` +rather than a hand-written `minor_centi`, per that field's own doc (`4` and +`40` look interchangeable at a glance and are not). + +**Blast radius: no wire change.** `ChunkKind::LayoutCache` is a regenerable +major-0 role; this is a discard-and-regenerate identity, not a decoded one. +The one line that changes emitted bytes is `resolved.rs`'s `encode_catalog` +(`.minor` → `.minor_centi`); `solver.rs`'s `forged_catalog_metadata_is_rejected` +mutates the same field (`+= 1`) to build a still-nonsense version, unchanged +in intent. The PASS13_CANDIDATES.md P13-S12 entry's claim that the move +lands "with golden/vector regen" was verified false and corrected in +place — no golden, baseline, or vector is pinned to the catalog identity +anywhere in the workspace (every `canonical_bytes()` assertion is relative). + +**The generator regains meaning too**: `epiphany-testkit`'s +`gen_smufl_version` used to draw `minor: rng.range(0, 6)`, which under the +now-shared fraction-normalized field would denote 1.00–1.05 — versions SMuFL +never released. It now samples a minor-digit string from the real release +set (`"12", "18", "20", "3", "4"`) through `SmuflVersion::from_decimal`, so +it can never emit a value the type's own invariant forbids. + +**Locked by** `catalog_identity_smufl_version_orders_the_real_release_sequence` +(`glyph.rs`), which builds five `GlyphCatalogIdentity`s from the real release +digits and asserts `1.12 < 1.18 < 1.20 < 1.3 < 1.4`. Mutation-verified by +hand: reverting the identities' construction to store the digit strings +literally in `minor_centi` (simulating the deleted type's storage) fails the +test at the first backwards pair (`1.20` sorting after `1.3`), confirming +the assertion is not vacuous. + +**Spec**: `core_spec.tex` gains the `SmuflVersion` type listing near +`SmuflVersionRequirement` (§"SMuFL Versioning", Chapter 4) that P13-S12 +reached Rust and the wire but never the core specification for — a type +listing with normative prose (fraction-normalized minor, the real release +table as a non-normative note) and no new `\label{req:...}` (counts stay +212/282/282). Both Chapter 9 usages (`GlyphCatalog::smufl_version()`, +`GlyphCatalogIdentity.smufl_version`) gain a cross-reference sentence back to +this single definition. diff --git a/crates/epiphany-layout-ir/src/glyph.rs b/crates/epiphany-layout-ir/src/glyph.rs index 20709a9..9de9baf 100644 --- a/crates/epiphany-layout-ir/src/glyph.rs +++ b/crates/epiphany-layout-ir/src/glyph.rs @@ -25,11 +25,16 @@ use epiphany_determinism::{DomainTag, Preimage}; use crate::spatial::{BoundingBox, Point}; /// The SMuFL version a catalog targets (Chapter 7: `SmuflVersion`). -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct SmuflVersion { - pub major: u16, - pub minor: u16, -} +/// +/// This is [`epiphany_core::SmuflVersion`] (Chapter 4 §"SMuFL Versioning"): +/// the glyph catalog's version and the tuning context's declared SMuFL +/// version are **the same type**, unified here in Push 4b tranche 3b-ii +/// (P13-S12). Layout-ir previously defined its own `{ major: u16, minor: u16 +/// }` storing the minor *literally*, whose derived `Ord` sorted 1.3/1.4 +/// before 1.12 — backwards versus SMuFL's real release order. See +/// [`epiphany_core::SmuflVersion`]'s doc comment for the fraction-normalized +/// `minor_centi` shape that fixes it. +pub use epiphany_core::SmuflVersion; /// Identifier of a specific SMuFL font (Chapter 7: `FontId`). #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] @@ -117,7 +122,8 @@ impl Default for GlyphCatalogIdentity { /// one over only the glyphs it references (the solve's true inputs). fn default() -> Self { GlyphCatalogIdentity { - smufl_version: SmuflVersion { major: 1, minor: 4 }, + smufl_version: SmuflVersion::from_decimal(1, "4") + .expect("1.4 is a valid SMuFL version"), font_id: FontId::BRAVURA, font_version: Some(BRAVURA_VERSION), metrics_hash: metrics_hash_for(BRAVURA_METRICS.iter().map(|m| m.name.as_ref())), @@ -329,7 +335,7 @@ impl GlyphCatalog for BravuraCatalog { } fn smufl_version(&self) -> SmuflVersion { - SmuflVersion { major: 1, minor: 4 } + SmuflVersion::from_decimal(1, "4").expect("1.4 is a valid SMuFL version") } fn identity(&self, consulted: &[&str]) -> GlyphCatalogIdentity { @@ -423,6 +429,39 @@ mod tests { ); } + /// P13-S12 / Push 4b tranche 3b-ii: the live bug this tranche fixes. + /// Layout-ir used to define its own `SmuflVersion { major: u16, minor: + /// u16 }` storing the minor digit *literally*, so derived `Ord` sorted + /// 1.3 and 1.4 **before** 1.12 — backwards versus SMuFL's real release + /// order (1.12 → 1.18 → 1.20 → 1.3 → 1.4). Exercised through + /// [`GlyphCatalogIdentity::smufl_version`], not the bare type, because + /// that field is the layout-conformance identity the bug actually lived + /// on. + /// + /// Non-vacuous: reverting `crate::SmuflVersion` to the deleted literal-minor + /// shape (`{ major: u16, minor: u16 }` with derived `Ord`) and rebuilding + /// each identity as `{ major: 1, minor: }` makes this + /// test fail — `{1,3}` and `{1,4}` sort *before* `{1,12}`, breaking the + /// third assertion in the loop below. Verified by hand against the + /// pre-unification type before it was deleted. + #[test] + fn catalog_identity_smufl_version_orders_the_real_release_sequence() { + let identity_with = |minor_digits: &str| GlyphCatalogIdentity { + smufl_version: SmuflVersion::from_decimal(1, minor_digits) + .expect("valid SMuFL version"), + ..GlyphCatalogIdentity::default() + }; + let sequence = ["12", "18", "20", "3", "4"].map(identity_with); + for pair in sequence.windows(2) { + assert!( + pair[0].smufl_version < pair[1].smufl_version, + "{:?} did not sort before {:?} in SMuFL's real release order", + pair[0].smufl_version, + pair[1].smufl_version + ); + } + } + #[test] fn metrics_hash_is_order_and_duplicate_independent() { let a = metrics_hash_for(["gClef", "noteheadBlack", "accidentalSharp"]); @@ -516,7 +555,7 @@ mod tests { None } fn smufl_version(&self) -> SmuflVersion { - SmuflVersion { major: 1, minor: 4 } + SmuflVersion::from_decimal(1, "4").expect("1.4 is a valid SMuFL version") } fn identity(&self, _consulted: &[&str]) -> GlyphCatalogIdentity { GlyphCatalogIdentity { diff --git a/crates/epiphany-layout-ir/src/resolved.rs b/crates/epiphany-layout-ir/src/resolved.rs index dfad217..69bd306 100644 --- a/crates/epiphany-layout-ir/src/resolved.rs +++ b/crates/epiphany-layout-ir/src/resolved.rs @@ -361,7 +361,7 @@ fn encode_decision(out: &mut Vec, d: &EngravingDecision) { fn encode_catalog(out: &mut Vec, c: &GlyphCatalogIdentity) { out.extend_from_slice(&c.smufl_version.major.to_le_bytes()); - out.extend_from_slice(&c.smufl_version.minor.to_le_bytes()); + out.extend_from_slice(&c.smufl_version.minor_centi.to_le_bytes()); let font = c.font_id.0.as_bytes(); push_len(out, font.len()); out.extend_from_slice(font); diff --git a/crates/epiphany-layout-ir/src/solver.rs b/crates/epiphany-layout-ir/src/solver.rs index 5c510a2..0f43ca5 100644 --- a/crates/epiphany-layout-ir/src/solver.rs +++ b/crates/epiphany-layout-ir/src/solver.rs @@ -766,7 +766,7 @@ mod tests { assert!(!report.satisfied_hard_constraints); let mut input = constrained(vec![glyph("noteheadBlack")]); - input.catalog.smufl_version.minor += 1; + input.catalog.smufl_version.minor_centi += 1; assert_eq!( StubSolver.solve(&input, &SolverConfig::default()).status, SolveStatus::InternalError diff --git a/crates/epiphany-testkit/src/layout_stub.rs b/crates/epiphany-testkit/src/layout_stub.rs index 591095b..4c5d50b 100644 --- a/crates/epiphany-testkit/src/layout_stub.rs +++ b/crates/epiphany-testkit/src/layout_stub.rs @@ -794,12 +794,17 @@ pub fn gen_glyph_metric(rng: &mut Rng) -> GlyphMetric { BRAVURA_METRICS[rng.below(BRAVURA_METRICS.len() as u64) as usize].clone() } -/// A SMuFL version. +/// A SMuFL version. `minor_centi` is fraction-normalized (P13-S12): a small +/// integer sampled directly (as the previous generator did) would denote +/// 1.00-1.05 as hundredths, versions SMuFL never released. Instead sample the +/// minor digit string from real SMuFL minor releases and normalize it through +/// [`SmuflVersion::from_decimal`], the same constructor production code uses, +/// so the generator can never emit a value the type's own invariant forbids. pub fn gen_smufl_version(rng: &mut Rng) -> SmuflVersion { - SmuflVersion { - major: rng.range(1, 2) as u16, - minor: rng.range(0, 6) as u16, - } + const MINOR_RELEASES: [&str; 5] = ["12", "18", "20", "3", "4"]; + let major = rng.range(1, 2) as u16; + let minor_digits = MINOR_RELEASES[rng.below(MINOR_RELEASES.len() as u64) as usize]; + SmuflVersion::from_decimal(major, minor_digits).expect("a real SMuFL minor release") } /// A font identifier (the bundled font). diff --git a/spec/CONTRACT_PUSH4B_3BII_IDENTITY.md b/spec/CONTRACT_PUSH4B_3BII_IDENTITY.md new file mode 100644 index 0000000..fb95183 --- /dev/null +++ b/spec/CONTRACT_PUSH4B_3BII_IDENTITY.md @@ -0,0 +1,180 @@ +# 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. diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index ac4d4e7..bb25c30 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -85,4 +85,4 @@ open. | P13-S9 | **The citation checker enforces cited→defined, never cited→relevant.** `requirement_labels.rs` proves every `req:*` string in the repo resolves to a real label and nothing further, so a citation that resolves cleanly but does not support the sentence it is attached to is invisible to a fully green gate. Three instances in one day: a Push-4b spec agent justified "resolved layout is non-canonical" with `req:layoutir:staff-space-coordinates`, which mandates staff-space units and `f32` and says nothing about canonicality; a dispatch contract asserted "Chapter 10 defines `KeySignature`" when the Score Graph is Chapter 5 (caught only because the agent used `\ref{ch:graph}` and reported the discrepancy rather than hardcoding either number); and **P13-S4 is itself an instance** — an agent "corrected" two dangling citations to the ownership requirements, replacing a visibly broken pointer with a silently wrong one | this file | **open** (relevance is not mechanically checkable, so the ratification is *what discipline replaces a check*: e.g. requiring the quoted sentence alongside any newly-added citation so review compares text to text, or treating agent-authored citations as unverified until a human reads the cited requirement. Note the asymmetry that makes this dangerous — the broken-pointer case is loud and the wrong-pointer case is silent, so **fixing the loud one without reading the target converts a caught defect into an uncaught one**) | | P13-S10 | **`PitchSpaceModification::Cents(f64)` puts a raw `f64` in canonical state, which the byte layer cannot encode.** `core_spec.tex:3112` declares `Cents(f64)`; `PitchSpaceModification` is reached from canonical score state through `AccidentalDefinition` → `ScoreAccidentalExtensions` → `ScoreTuningContext`. `req:determinism:canonical-floating-point` requires canonical stored floats to be finite IEEE 754 binary64, and the byte layer enforces it with no escape hatch: `serialize.rs:110` decodes floats *only* through `CanonicalF64::from_le_bytes → NonFiniteFloat`, so there is no `Codec for f64`. A raw `f64` is therefore not merely risky in canonical state — it is unencodable without inventing a new unvalidated codec. The same chapter already resolved this exact tension forty lines later: `EngravingBoundingBox` (`:3150`) carries `SpaceUnit` edges with a rationale (`:3191`) invoking the same requirement | this file (verified 2026-07-23 against `core_spec.tex:3112`, `:3150-3197`, and `crates/epiphany-determinism/src/serialize.rs:110`) | **resolved** (ratified: `Cents(CanonicalF64)`. A one-line spec correction, the only shape consistent with the existing byte layer — the identical maneuver Ruling D applied to the bounding box. Freezes in tranche 3b) | | P13-S11 | **`AnchorPoint`, referenced by `AccidentalEngraving.anchor`, is defined nowhere.** `core_spec.tex:3166` names `pub anchor: AnchorPoint`; no struct or enum of that name exists in the spec, and `epiphany-core` (whose `Cargo.toml` has no `epiphany-layout-ir` dependency) cannot borrow any layout-ir type even if one shared the name — the same core-native requirement that forced `EngravingBoundingBox`. An undefined leaf frozen onto the wire is the `KeyContext`-shaped gap. Compounding it: the bounding box is documented "relative to the glyph's anchor point" (`:3160`), so freezing an anchor with no defined coordinate frame freezes a point with an undefined origin | this file (verified 2026-07-23; `AnchorPoint` appears once in `core_spec.tex`, defined nowhere; `epiphany-core/Cargo.toml` lists no layout-ir) | **resolved** (ratified: core-native `AnchorPoint { x: SpaceUnit, y: SpaceUnit }`, over the same `SpaceUnit` as `advance_width` and `EngravingBoundingBox`. **Plus one normative sentence pinning the frame**: x/y in canonical space units, y-up, relative to the glyph's coordinate origin — matching the repo's existing Bravura outline convention — so the anchor and the box it anchors share an unambiguous origin. Freezes in tranche 3b) | -| P13-S12 | **`SmuflVersion` is undefined, and its obvious representation orders SMuFL's real history backwards.** `SmuflVersionRequirement` (`core_spec.tex:3269`) carries `minimum`/`authored_against` of type `SmuflVersion`, which Chapter 4 references but does not define (it exists only as a **Chapter 7 / layout-ir** type, `glyph.rs:29`, with literal-minor encoding — see the resolution). Ordering is load-bearing (`SmuflVersionRequirement.minimum`, `:3271`, gates the fallback of `req:tuning:smufl-version-fallback`), and the type is dual-purpose — it also anchors Chapter 9's `GlyphCatalog::smufl_version()` / `GlyphCatalogIdentity` (`:10420`, `:10460`), so this freeze touches layout-conformance identity, not just tuning. The trap: SMuFL versions are decimal fractions — 1.12 (2015), 1.18, 1.20 (2016), 1.3 (2019), 1.4 (2021) — that succeed by fraction (0.12 < 0.18 < 0.20 < 0.30 < 0.40). A `{ major: u16, minor: u16 }` storing literal digits with derived `Ord` orders the minors 3 < 4 < 12 < 18 < 20, placing 1.3 and 1.4 **before** 1.12; old fonts declaring 1.18-era versions exist forever | this file (verified 2026-07-23 against `core_spec.tex:3269-3274` and SMuFL's published release history) | **resolved** (ratified shape `SmuflVersion { major: u16, minor_centi: u16 }`, **literal-minor storage rejected**: the minor is stored **fraction-normalized to hundredths** — 1.12→(1,12), 1.18→(1,18), 1.20→(1,20), 1.3→(1,30), 1.4→(1,40), rule normative, release table a note. Derived `Ord` is then correct across the whole real history and collapses the 1.2/1.20 ambiguity (both → (1,20)). **Correction on filing: the leaf is NOT undefined — it exists in `epiphany-layout-ir/src/glyph.rs:29` as `SmuflVersion { major, minor }` with LITERAL minor (`{1,4}`) and derived `Ord`, so the backwards-ordering bug is LIVE there today (1.3 < 1.12), and it is a direct field of `GlyphCatalogIdentity` — conformance identity.** `epiphany-core` cannot depend on layout-ir, and `SmuflVersionRequirement` is core, so the type MUST be defined in core and layout-ir must reuse it — a **unification**, not a fresh definition, which moves `GlyphCatalogIdentity` (`{1,4}`→`minor_centi 40`). Tranche 3a defines `core::SmuflVersion` for the tuning use and leaves layout-ir's alone (a bounded, core-invisible homonym since core can't import layout-ir's); tranche 3b performs the unification and the deliberate `GlyphCatalogIdentity` move with golden/vector regen. The hundredths scale is blocking for 3b, free to adjust in 3a) | +| P13-S12 | **`SmuflVersion` is undefined, and its obvious representation orders SMuFL's real history backwards.** `SmuflVersionRequirement` (`core_spec.tex:3269`) carries `minimum`/`authored_against` of type `SmuflVersion`, which Chapter 4 references but does not define (it exists only as a **Chapter 7 / layout-ir** type, `glyph.rs:29`, with literal-minor encoding — see the resolution). Ordering is load-bearing (`SmuflVersionRequirement.minimum`, `:3271`, gates the fallback of `req:tuning:smufl-version-fallback`), and the type is dual-purpose — it also anchors Chapter 9's `GlyphCatalog::smufl_version()` / `GlyphCatalogIdentity` (`:10420`, `:10460`), so this freeze touches layout-conformance identity, not just tuning. The trap: SMuFL versions are decimal fractions — 1.12 (2015), 1.18, 1.20 (2016), 1.3 (2019), 1.4 (2021) — that succeed by fraction (0.12 < 0.18 < 0.20 < 0.30 < 0.40). A `{ major: u16, minor: u16 }` storing literal digits with derived `Ord` orders the minors 3 < 4 < 12 < 18 < 20, placing 1.3 and 1.4 **before** 1.12; old fonts declaring 1.18-era versions exist forever | this file (verified 2026-07-23 against `core_spec.tex:3269-3274` and SMuFL's published release history) | **resolved** (ratified shape `SmuflVersion { major: u16, minor_centi: u16 }`, **literal-minor storage rejected**: the minor is stored **fraction-normalized to hundredths** — 1.12→(1,12), 1.18→(1,18), 1.20→(1,20), 1.3→(1,30), 1.4→(1,40), rule normative, release table a note. Derived `Ord` is then correct across the whole real history and collapses the 1.2/1.20 ambiguity (both → (1,20)). **Correction on filing: the leaf is NOT undefined — it exists in `epiphany-layout-ir/src/glyph.rs:29` as `SmuflVersion { major, minor }` with LITERAL minor (`{1,4}`) and derived `Ord`, so the backwards-ordering bug is LIVE there today (1.3 < 1.12), and it is a direct field of `GlyphCatalogIdentity` — conformance identity.** `epiphany-core` cannot depend on layout-ir, and `SmuflVersionRequirement` is core, so the type MUST be defined in core and layout-ir must reuse it — a **unification**, not a fresh definition, which moves `GlyphCatalogIdentity` (`{1,4}`→`minor_centi 40`). Tranche 3a defines `core::SmuflVersion` for the tuning use and leaves layout-ir's alone (a bounded, core-invisible homonym since core can't import layout-ir's); tranche 3b performs the unification and the deliberate `GlyphCatalogIdentity` move. **Correction (2026-07-23, tranche 3b-ii): "with golden/vector regen" above was verified false before that tranche's dispatch.** No golden, baseline, or vector is pinned to the catalog identity — every assertion on `ResolvedLayoutIR::canonical_bytes()` is relative (stability, determinism, and a `metrics_hash[0] ^= 1` sensitivity check that never touches `smufl_version`), and the committed SVG/PNG goldens do not embed it. The move changes the catalog's emitted bytes (`encode_catalog`'s minor field: `04 00` → `28 00`) in *value*, with nothing to regenerate. The hundredths scale is blocking for 3b, free to adjust in 3a) | diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index a0d5033..1fcefbd 100644 Binary files a/spec/core_spec.pdf and b/spec/core_spec.pdf differ diff --git a/spec/core_spec.tex b/spec/core_spec.tex index 85618e6..461976b 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -3265,6 +3265,41 @@ pub enum GlyphReference { indicator); it \MUSTNOT{} silently fail. \end{requirement} +\begin{lstlisting}[language=Rust] +pub struct SmuflVersion { + pub major: u16, + + /// The fractional part, normalized to hundredths. + pub minor_centi: u16, +} +\end{lstlisting} + +A SMuFL version's fractional part is stored \emph{normalized to +hundredths} in \texttt{minor\_centi}, not as the literal digits that +follow the decimal point: 1.12 $\to$ \tablenums{12}, 1.18 $\to$ +\tablenums{18}, 1.20 $\to$ \tablenums{20}, 1.3 $\to$ \tablenums{30}, +1.4 $\to$ \tablenums{40}. SMuFL versions are decimal fractions, and +this repository's published minor releases did not arrive in digit +order: 1.12 and 1.18 shipped before 1.3 and 1.4. A literal-digit +encoding (\texttt{minor: 3} for 1.3, \texttt{minor: 12} for 1.12) is +\textbf{non-conforming} because the derived ordering on +\texttt{(major, minor)} then sorts 1.3 and 1.4 \emph{before} 1.12, +backwards versus the real release history. The hundredths +normalization also collapses the 1.2/1.20 spelling ambiguity: both +denote the same release and both normalize to \tablenums{20}. This is +the single \texttt{SmuflVersion} type used throughout the +specification --- by \texttt{SmuflVersionRequirement} below and, in +Chapter~\ref{ch:layout-ir}, by \texttt{GlyphCatalog::smufl\_version()} +(Section~\ref{sec:layoutir:catalog}) and \texttt{GlyphCatalogIdentity} +(Section~\ref{sec:layoutir:catalog-identity}). + +\begin{rationale} + For reference, SMuFL's minor releases in their real order, each with + its normalized \texttt{minor\_centi} value: 1.12 + (\tablenums{12}), 1.18 (\tablenums{18}), 1.20 (\tablenums{20}), 1.3 + (\tablenums{30}), 1.4 (\tablenums{40}). +\end{rationale} + \begin{lstlisting}[language=Rust] pub struct SmuflVersionRequirement { /// Minimum SMuFL version required by this score. @@ -10406,7 +10441,10 @@ expressed in: The IR references glyphs by identifier; glyph metrics are queried from a font catalog at IR construction. This section defines the interface; the catalog implementation is outside the core -specification. +specification. \texttt{SmuflVersion}, below, is the same type declared +by a score's \texttt{SmuflVersionRequirement} +(Section~\ref{sec:tuning:smufl}): the glyph catalog's version and the +tuning context's declared version are one type, not two. \begin{lstlisting}[language=Rust] pub trait GlyphCatalog: Send + Sync { @@ -10449,7 +10487,10 @@ pub struct GlyphRenderData { \label{sec:layoutir:catalog-identity} Layout output depends on glyph metrics. For reproducible layout, -the exact glyph catalog used \MUST{} be identifiable. +the exact glyph catalog used \MUST{} be identifiable. \texttt{smufl\_version} +below is a \texttt{SmuflVersion} (Section~\ref{sec:tuning:smufl}), ordered +by its fraction-normalized \texttt{minor\_centi} so a catalog identity's +version compares correctly against SMuFL's real release history. \begin{lstlisting}[language=Rust] /// A reproducibility-quality identifier for the glyph catalog