From 91d44dbe65cfad41e826d9399f1481ec8fe3c1fb Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 24 Jul 2026 14:23:36 -0400 Subject: [PATCH] P13-S13 defers to the genesis-persistence ruling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ratified 2026-07-24. S13 is re-pointed rather than resolved: the tuning context is not a special case, it is field 10 of an eight-field gap that spec/ANALYSIS_GENESIS_PERSISTENCE.md tables across the whole Score — canvas.layout_defaults, instruments, staff_groups, parts, tuning_context, spelling_precedence, analysis_layers, views, plus identity, with decomposition_attachments prunable-but-never-authored. Each is reachable only through a base the reducer is handed, and pruning is licensed to replace that base with a MaterializedState carrying none of them. Independently re-verified against the working tree before recording: every one of those fields' sole reduce.rs mention is a read-only base-seed read, and epiphany-ops has no tuning-context payload at all. So the entry drops the operation-vs-canonical-base framing it was filed with. That framing was right about the axis and wrong about the scope: it maps onto the analysis's dispositions B and C at eight times the size, and a tuning-specific fix is exactly the shape the analysis argues against — "any option that enumerates fields must be re-audited against this table every time a field is added to Score". S13 is now marked blocked-on rather than open, and whichever disposition the genesis ruling picks resolves it as a side effect. The accept-set cost this entry surfaced is kept, repositioned as evidence against enumerated per-field fixes rather than as a tuning-only tradeoff: blocks stamp minimally, and bundle.rs caps OperationEnvelopeBlock at major 2 precisely because no operation payload embeds the tuning context, so a SetTuningContext-style operation would drag a role accept-set raise along with the kind append — for one field of eight. The wire layouts stay frozen and correct under every disposition. This was never about how the tuning context encodes, only about which carrier embeds it, so epiphany-core's DECISIONS entry now records the matching prohibition: no SetTuningContext operation, and no widening of ScoreTuningContext's layout to compensate. Docs only; no code path touched. Gate unchanged: fmt clean, 1336 passed / 0 failed, requirement labels 6/6 at 212/282/282. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV --- crates/epiphany-core/DECISIONS.md | 20 ++++++++++++++++++++ spec/PASS13_CANDIDATES.md | 12 +++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index c79ff79..68a0995 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -1365,3 +1365,23 @@ made the *acceleration snapshot* able to carry it, and that snapshot is explicitly non-canonical and regenerable. So a tuning override survives a snapshot round-trip but cannot be authored, replicated, or merged. Filed as a Pass-13 candidate; it is a data-model question, not a codec one. + +**Ratified 2026-07-24: S13 defers to the genesis-persistence ruling, and +`epiphany-core` must not grow a tuning-specific fix for it.** The same gap runs +across the whole `Score` — `spec/ANALYSIS_GENESIS_PERSISTENCE.md` tables eight +fields no operation can produce (`canvas.layout_defaults`, `instruments`, +`staff_groups`, `parts`, `tuning_context`, `spelling_precedence`, +`analysis_layers`, `views`, plus `identity`), each reachable only through a base +the reducer is handed, and pruning is licensed to replace that base with a +`MaterializedState` carrying none of them. The tuning context is field 10 of +that table, not a special case. Whatever disposition that ruling picks resolves +this as a side effect. + +Concretely for this crate: **do not add a `SetTuningContext` operation, and do +not widen `ScoreTuningContext`'s wire layout to compensate.** The layout is +already frozen and correct under every disposition — the open question is which +carrier embeds it, never how it encodes. And the per-field operation route +carries a cost worth remembering: blocks stamp minimally, and `bundle.rs` caps +`OperationEnvelopeBlock` at major 2 *because* no operation payload embeds the +tuning context, so such an operation would drag a role accept-set raise along +with it — for one field of eight. diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index 9d35fe4..1fc9adf 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -69,10 +69,12 @@ whose obvious representation orders SMuFL's real release history backwards. All three are ratified; each freezes forever once tranche 3b lands, so they were resolved before dispatch, not after. -S4, S8, S9, and S13 remain open; S10–S12 are resolved on ratification. The pass -stays open. (S13 was filed 2026-07-24, after S10–S12's wire freeze landed: it is -the gap that freeze made visible — a value with a wire form and no canonical -carrier to reach it.) +S4, S8, and S9 remain open; S10–S12 are resolved on ratification; S13 is +**blocked on the genesis-persistence ruling** rather than open, because it is one +field of an eight-field gap and a tuning-specific fix would be the wrong shape +(`spec/ANALYSIS_GENESIS_PERSISTENCE.md`). The pass stays open. (S13 was filed +2026-07-24, after S10–S12's wire freeze landed: it is the gap that freeze made +visible — a value with a wire form and no canonical carrier to reach it.) | Id | One-line statement | Filed in | Status | |---|---|---|---| @@ -88,4 +90,4 @@ carrier to reach it.) | 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. **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) | -| P13-S13 | **The score tuning context has no canonical persistence path: it can be saved, but never authored, replicated, or merged.** Schema major 3 (Push 4b tranche 3b-i) put `smufl` and `overrides` on the wire, but the only *persisted* carrier that embeds a `ScoreTuningContext` is the **acceleration snapshot**, which Chapter 8 makes explicitly non-canonical and regenerable and which the bundle may discard and rebuild at will. No **canonical** carrier embeds it at all. The two carriers that *are* canonical do not carry it: no operation authors it (`epiphany-ops` has no tuning-context payload anywhere — the vocabulary's only `tuning` references are the per-pitch `TuningReference::Inherit`), and `MaterializedState` carries effects, conflicts, anomalies, objects, spellings, breaks, page-breaks, and pending, but no tuning context. So a user who selects a tuning system or sets a per-voice override has authored something the format cannot durably represent: it survives a snapshot round-trip, and is lost the moment the snapshot is regenerated from the operation log. This also means `req:tuning:tuning-resolution-order`'s scopes 2–4 (`overrides`) are, in practice, unreachable from any document a replica could exchange | this file (found 2026-07-24 while scoping text-projection parity; verified by searching the whole `epiphany-ops` operation vocabulary and `MaterializedState`'s field list) | **open** (ratification needed on *where* it belongs: an authoring operation kind — a schema-minor kind append, per the `DeleteRepeatStructure`/`TransposeInterval` precedent — versus a field on the canonical base, which is a data-model major. The operation route is the cheaper mechanism and the one every other authored score property already uses; the base route would make it reduced state rather than authored state, which is likely wrong since a user *chooses* a tuning. Note the wire layouts are already frozen and correct either way — this is about which carrier embeds them, not how they encode. **One cost the operation route must price in:** blocks are stamped *minimally*, at the lowest major whose layouts decode them, and `bundle.rs`'s `max_supported_major` currently caps `OperationEnvelopeBlock` at **2** on the explicit ground that no operation payload embeds the tuning context, so no op block is ever born at v3. An authoring operation carrying a `TuningOverride` would make its blocks stamp v3, so the kind append — a schema *minor* in itself — drags a *role accept-set raise* along with it. That is a small, well-precedented change, but it is not free, and it is the one asymmetry that makes the two routes closer in cost than they first appear. Not a regression: nothing ever worked; major 3 made the gap visible by giving the value a wire form and no way to reach it) | +| P13-S13 | **The score tuning context has no canonical persistence path: it can be saved, but never authored, replicated, or merged.** Schema major 3 (Push 4b tranche 3b-i) put `smufl` and `overrides` on the wire, but the only *persisted* carrier that embeds a `ScoreTuningContext` is the **acceleration snapshot**, which Chapter 8 makes explicitly non-canonical and regenerable and which the bundle may discard and rebuild at will. No **canonical** carrier embeds it at all: no operation authors it (`epiphany-ops` has no tuning-context payload anywhere — the vocabulary's only `tuning` references are the per-pitch `TuningReference::Inherit`), and `MaterializedState` carries effects, conflicts, anomalies, objects, spellings, breaks, page-breaks, and pending, but no tuning context. So a user who selects a tuning system or sets a per-voice override has authored something the format cannot durably represent, and `req:tuning:tuning-resolution-order`'s scopes 2–4 (`overrides`) are in practice unreachable from any document a replica could exchange | this file (found 2026-07-24 while scoping text-projection parity; verified by searching the whole `epiphany-ops` operation vocabulary and `MaterializedState`'s field list) | **blocked on the genesis-persistence ruling** (ratified 2026-07-24). **This is not a tuning-specific defect and MUSTNOT get a tuning-specific fix.** `spec/ANALYSIS_GENESIS_PERSISTENCE.md` maps the same gap across the whole `Score`: **eight fields have no operation that can produce them** — `canvas.layout_defaults`, `instruments`, `staff_groups`, `parts`, `tuning_context` (this entry, its field 10), `spelling_precedence`, `analysis_layers`, `views`, plus `identity` — and one more (`decomposition_attachments`) can only be pruned back, never authored. Independently re-verified against the working tree: each field's sole `reduce.rs` mention is a read-only base-seed read. The disposition chosen there (a canonical genesis block; closing the op-coverage gap; promoting the canonical base to carry graph values; or scope-limiting) resolves this entry as a side effect, so S13 is tracked *there*, not solved here. **One cost this entry contributes as evidence:** the per-field operation route is not the free schema-minor it appears. Blocks stamp *minimally*, at the lowest major whose layouts decode them, and `bundle.rs`'s `max_supported_major` currently caps `OperationEnvelopeBlock` at **2** on the explicit ground that no operation payload embeds the tuning context. A `SetTuningContext`-style operation would make its blocks stamp v3, dragging a role accept-set raise along with the kind append — for one field of eight. That is a concrete instance of the analysis's closing constraint, that any option enumerating fields must be re-audited against its table whenever `Score` gains a field. Note the wire layouts themselves are already frozen and correct under every disposition: this was never about how the tuning context encodes, only about which carrier embeds it. Not a regression — nothing ever worked; major 3 made the gap visible by giving the value a wire form and no way to reach it) |