diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index 68a0995..029d00e 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -1385,3 +1385,19 @@ 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. + +**Superseded 2026-07-24 by `spec/RULING_GENESIS_PERSISTENCE.md` (011c68a).** The +ruling reverses Pass-12 K8 and absorbs genesis into the operation set, so +`SetTuningContext` **is** the disposition — arriving not as the tuning-specific +fix this note forbade, but as one of nine surfaces in a single coordinated +tranche. Read the prohibition as scoped to what it was aimed at: no tuning-only +operation, and no widening of the wire layout to compensate for a missing +carrier. Both still hold. The layout stays frozen exactly as this note says — +the tranche changes which carrier embeds the value, never how it encodes. + +The accept-set cost above is unchanged as a *fact* and is now **spent +deliberately**: the ruling's "one accept-set raise, spent once" lands every new +kind in one batch at `OperationEnvelopeBlock` major 3, so the raise is amortised +across nine surfaces rather than charged to one field of eight. Note that +`bundle.rs`'s cap of 2 is documented *with that rationale in prose* — when the +tranche lands, that comment becomes false and must move with the cap. diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index 1fc9adf..421bad5 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -70,8 +70,8 @@ three are ratified; each freezes forever once tranche 3b lands, so they were resolved before dispatch, not after. 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 +**resolved by the genesis tranche**, not by this pass — it is one field of an +eight-field gap, and a tuning-specific fix would have been 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.) @@ -90,4 +90,4 @@ visible — a value with a wire form and no canonical 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: 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) | +| 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) | **resolved by the genesis tranche** — `spec/RULING_GENESIS_PERSISTENCE.md` (ratified 2026-07-24, 011c68a) reverses Pass-12 K8 and absorbs genesis into the operation set, naming `SetTuningContext` in its §2 settings table. **This was not a tuning-specific defect and did not get a tuning-specific fix:** the operation arrives as one of nine surfaces in a single coordinated tranche, and the wire layout is untouched. **The disposition, and the evidence that produced it:** `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. Of the four dispositions weighed there (a canonical genesis block; closing the op-coverage gap; promoting the canonical base to carry graph values; scope-limiting), **the second was taken** — every mutable `Score` field becomes operation-authored — so S13 closes when that tranche lands and is tracked *there*, not 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. **This cost is now paid deliberately rather than avoided:** the ruling's "one accept-set raise, spent once" lands every new kind as a single batch at `OperationEnvelopeBlock` major 3, amortising the raise across nine surfaces instead of charging it to this one. Note `bundle.rs` documents the cap of 2 *with the tuning-context rationale in prose*, so that comment must move with the cap. 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) | diff --git a/spec/RULING_GENESIS_PERSISTENCE.md b/spec/RULING_GENESIS_PERSISTENCE.md index c070f4f..cd7456a 100644 --- a/spec/RULING_GENESIS_PERSISTENCE.md +++ b/spec/RULING_GENESIS_PERSISTENCE.md @@ -98,7 +98,8 @@ the tranche. ### Out of scope, with reasons -* **`identity`** — not an authored field; see §3, which must be settled first. +* **`identity`** — not an authored field; ruled in §3. It stays on `Score` and + on the wire, and reduction derives its counter. Nothing to author. * **`decomposition_attachments`** — **derived, not authored.** The prepass creates it (`core/src/prepass.rs:382`); reduction only ever *retains* (`reduce.rs:2342`). It leaves the table rather than gaining operations. @@ -106,9 +107,7 @@ the tranche. --- -## 3. The one open sub-decision, and it blocks specification - -**`identity` must be dispositioned before the tranche can be specified.** +## 3. The `identity` sub-decision — **RULED 2026-07-24** `IdentityContext { replica_id, next_counter }` is replica-scoped by construction (`ids.rs:711-717`) yet lives on `Score`, which the codec @@ -118,10 +117,70 @@ shared base, which masks the tension. Under this ruling reduction runs onto replicas reducing an identical log then produce Scores differing in an encoded field while the music is identical. -Options: move it to the manifest (document-scoped, recommended); keep it on -`Score` but exclude it from the canonical encoding; or accept -replica-dependent `Score` bytes and confine byte-equality claims to -`MaterializedState`. **Not ruled here.** +### The framing above understates it + +Verified against the working tree while scoping the tranche: + +* **`epiphany-ops` contains no `.identity` reference at all.** Reduction never + reads or writes it. `next_counter` advances only through + `IdentityContext::take_counter` (`ids.rs:766`), reached from `mint` / + `mint_operation`, which reduction never calls. +* **Invariant 11 does not bound the counter.** `invariants.rs:1746-1752` rejects + only the reserved `SYSTEM_DERIVED` replica; nothing checks `next_counter` + against the ids actually present in the score. +* **No production code mints from `score.identity` yet** — every such site is + under `#[cfg(test)]` (`editor-core/src/lib.rs:4124`). The hazard is latent, + and this tranche is what activates it. + +So `Score::identity` is an **authoring cursor that reduction never advances**. +Divergent bytes are the lesser problem. The real one: this tranche is precisely +when production code begins minting genesis-entity ids. Under from-empty the +cursor holds whatever seeded `Score::empty(identity)` — `0` for a fresh context +— while the log already carries that replica's ids at counters `0..N`. Minting +from it re-issues used counters, silently: no invariant, no reduction step, and +no wire check catches it. **Every option originally listed relocates or accepts +the field; none makes the cursor correct.** + +### The ruling + +**Accept replica-dependent `Score` bytes, and derive the cursor under +from-empty reduction.** + +1. `identity` **stays on `Score` and stays canonically encoded.** No wire + change, and in particular no schema major 4 on the Score/Snapshot role that + tranche 3b-i froze at 3. +2. **Byte-equality claims are confined to `MaterializedState`**, which carries + no identity field and whose `canonical_bytes` is already the asserted + convergence surface (`reduce.rs:10031`). §5's acceptance criterion is + *already* written against `MaterializedState`, so this costs no restatement. +3. **From-empty reduction sets `next_counter` to `1 + max(counter)` over ids + authored by the reducing replica in the log**, or leaves the seed untouched + when that replica authored none. A function of the log and the chosen + `replica_id`, hence convergent for a given replica and correct by + construction. + +Point 3 makes reduction write `identity` for the first time. Name that in the +tranche: it is a deliberate new behaviour, not an oversight, and it wants a +test that mints from a reduced score and asserts no collision with the log. + +### Why not the alternatives + +* **Manifest (previously recommended here).** Rejected. `req:format:manifest-id` + (`core_spec.tex:11177`) promises normatively that "two conforming writers + committing the same manifest body at the same generation of the same document + derive identical `ManifestId`s". A replica-scoped field in a shipped, + content-addressed manifest either breaks that promise or must be excluded from + the preimage — at which point it is one replica's counter riding to another in + a field no reader may trust. It also grows the one structure `bundle.rs:63` + records as never growing a versioned layout. +* **Exclude from the `Score` encoding.** A layout change under + `req:binfmt:frozen-layout` — schema major 4, a *second* wire raise on a + *different* role from §4's "one accept-set raise, spent once". And it leaves + the stale cursor standing. +* **Remove from `Score` entirely** (session state the editor owns). The cleanest + end state, and worth revisiting later; priced here at major 4 plus an API + break across `Score::empty` and every construction site, which this tranche + does not need to buy in order to be correct. ---