The identity cursor is ruled, and three records stop contradicting each other
RULING_GENESIS_PERSISTENCE.md §3 was the tranche's stated blocker. Ruled:
identity stays on Score and stays canonically encoded, byte-equality claims
confine to MaterializedState, and from-empty reduction derives next_counter
from the log.
Scoping it turned up that the section understated the problem. Verified in the
tree: epiphany-ops has no `.identity` reference at all, so reduction never
advances the cursor; invariant 11 checks only the reserved replica, never the
counter against ids present; and every mint from score.identity today is under
cfg(test). Score::identity is an authoring cursor reduction never touches, and
this tranche is what activates the hazard -- under from-empty the cursor sits at
the seed while the log already holds that replica's ids at 0..N. Divergent bytes
were the lesser problem, and none of the three options originally listed fixed
the larger one.
The manifest option, previously recommended, is rejected on evidence:
req:format:manifest-id promises two conforming writers derive identical
ManifestIds, which a replica-scoped field in a shipped content-addressed
structure cannot honour. The two wire options each cost schema major 4 on the
role 3b-i just froze at 3, and neither corrects the cursor.
Also reconciles two records against 011c68a. DECISIONS.md flatly prohibited a
SetTuningContext operation, which the ruling now requires; the prohibition is
marked superseded and re-scoped to what it was aimed at -- no tuning-only fix,
no wire widening to compensate -- both of which still hold. PASS13-S13 moves
from blocked-on to resolved-by, naming which of the four dispositions was taken.
Flagged for the tranche, not fixed here: bundle.rs documents the
OperationEnvelopeBlock cap of 2 with the tuning-context rationale in prose, so
that comment becomes false when the cap moves.
Gate: requirement_labels 6/6. No .md here is include_str'd or compiled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
parent
011c68a831
commit
ec17d06e77
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue