95 lines
33 KiB
Markdown
95 lines
33 KiB
Markdown
# Pass 13 — candidate ledger
|
||
|
||
Ambiguities and cross-cut inconsistencies found since Pass 12 closed, filed
|
||
per the house rule (a batch pass opens at ≥3 candidates; this file opened
|
||
when P13-D1/D2 joined P13-K1). Each entry names the owning DECISIONS record;
|
||
this file is the index, not the analysis.
|
||
|
||
## Batch 1 — CLOSED (2026-07-08)
|
||
|
||
All four candidates are resolved (worked down in order): P13-D3 and P13-K1 by
|
||
the user's ratified calls ("fix the mint only" / "reject the introduction"),
|
||
P13-D1 and P13-D2 as correctness fixes with convergence-locked /
|
||
execute-then-fix regressions.
|
||
|
||
| Id | One-line statement | Filed in | Status |
|
||
|---|---|---|---|
|
||
| P13-K1 | The K3 verdict for a system pitch introduced by a ModifyEvent replacement value differs across a snapshot cut (in-session `TargetMissing` vs post-snapshot `SystemDerivedContentImmutable`); really "may ModifyEvent introduce never-minted pitch ids" | `crates/epiphany-ops/DECISIONS.md` (Pass-12 G-pass code tranche) | **resolved** (Pass 13: reject the introduction — modify_event refuses a never-minted system-derived pitch, verdict now snapshot-cut-invariant; user "reject the introduction") |
|
||
| P13-D1 | Undo-driven event tombstones run graph-side re-anchor/cascade but never ledger-side `reanchor_for_tombstone`: structures leave the graph while staying `Live`, no `RepairRecord` — Ch6's same-step recording MUST is unmet for undo-driven tombstones (pre-existing class: slurs/spanners; repeats now too) | `crates/epiphany-ops/DECISIONS.md` (Schema major 2, Phase D) | **resolved** (Pass 13: `tombstone_undo_targets` runs the ledger re-anchor per event target; liveness guard; convergence-locked) |
|
||
| P13-D2 | Cue-cascade recursion re-anchors against the triggering event before its tombstone lands in `objects`: a structure anchored on {X, cue-of-X} can record `Reanchored{to: X}` then `CascadeDeleted` in one effect (contradictory repair trail; plausible by code trace, unexecuted) | `crates/epiphany-ops/DECISIONS.md` (Schema major 2, Phase D) | **resolved** (Pass 13: `delete_event` tombstones before the graph delete, matching `cascade_cue`/undo; repro executed then fixed) |
|
||
| P13-D3 | `CreateCrossCutting` validates only event endpoints (`CrossCuttingValue::endpoints()`), so a SPANNER anchored to a missing region/measure mints dangling past `anchor_target_exists`; and non-event referent tombstones (`DeleteRegion` under a region-anchored spanner/repeat) re-anchor nothing — "every referenced endpoint is live" is events-only as implemented | `crates/epiphany-ops/DECISIONS.md` (Phase D follow-up) | **resolved** (Pass 13: mint fixed via `anchor_object_refs`; non-event referent re-anchoring ratified events-only, user "fix the mint only") |
|
||
|
||
## Batch 2 — CLOSED (2026-07-09)
|
||
|
||
Three candidates accumulated while the Standard-tier solver track closed and the
|
||
notation-quality pass (stems, slurs) landed. All three were **parked** as they
|
||
were found, each in `crates/epiphany-layout-ir/DECISIONS.md`, and reaching three
|
||
reopened the pass per the house rule. None was a live incorrectness in shipped
|
||
output; each was a place where the code, the spec, and the data disagreed about
|
||
what is true.
|
||
|
||
**All three resolved**, worked down in order. Two grew when examined: P13-I1 was
|
||
filed as two elided fields and was three, the third (`diagnostics`) named nowhere
|
||
in core_spec; P13-I2's fix had to reach `editor-core` as well as the projection,
|
||
or a click on a bass staff would have resolved its pitch as treble. Zero golden
|
||
churn across all three. No open Pass-13 candidates remain; a future ≥3-candidate
|
||
batch reopens the pass.
|
||
|
||
| Id | One-line statement | Filed in | Status |
|
||
|---|---|---|---|
|
||
| P13-I1 | Chapter 7's `ConstrainedLayoutIR` listing elides **three** fields the code carries: `break_origins: Vec<BreakOrigin>` (named by `req:layoutir:break-origin-attribution`, its own shape unlisted), `catalog: GlyphCatalogIdentity` (its type specified, the field unlisted), and `diagnostics: Vec<LayoutDiagnostic>` — which appears **nowhere** in core_spec, though it is how the projection's honesty rule manifests: an unspellable pitch or an unbundled glyph is placed as a fallback *and recorded*, never silently guessed | `crates/epiphany-layout-ir/DECISIONS.md` ("the ConstrainedLayoutIR listing is still abridged") | **resolved** (Pass 13: listing gains all three fields; `BreakOrigin` and `LayoutDiagnostic` shapes added; new `req:layoutir:coverage-diagnostics` ratifies as-implemented that an unengravable object is recorded AND still placed — never guessed, never dropped) |
|
||
| P13-I2 | `Staff::default_clef` is never consulted: `to_constrained` takes the active clef from the staff instance's `clef_sequence` and falls back to `Clef::default()` (treble), so a bass-clef staff that declares its clef only on the `Staff` engraves as treble. The field is decorative in the projection — is it the fallback, or should it not exist? | `crates/epiphany-layout-ir/DECISIONS.md` ("`Staff::default_clef` is never consulted") | **resolved** (Pass 13: it IS the fallback. `StaffContent` carries it, `active_clef_or` resolves against it, and `editor-core`'s hit-test reads the same function — else a click on a bass staff would resolve its pitch as treble. Removal was rejected: the field is named for its purpose, is encoded on the wire, and dropping it is schema-major) |
|
||
| P13-I3 | `BRAVURA_METRICS`' `NOTEHEAD_ANCHORS` are hand-written, unconsumed, and doubly suspect: they name `stemUpNW`/`stemDownSE` — the corners a normal notehead's stems do *not* attach to, and a pair Bravura's `noteheadBlack` does not define — and their x of `1180` reads like 1.18 staff spaces written in thousandths rather than the table's `1/1024` units (1.18 sp = 1208). They enter only `metrics_hash`, so any correction moves the `GlyphCatalogIdentity` every conformance claim declares. The font is not vendored, so the values cannot be verified in-tree | `crates/epiphany-layout-ir/DECISIONS.md` ("the notehead stem anchors are unusable as written") | **resolved** (Pass 13: deleted. Unverifiable in-tree, unconsumed, and hand-derived where every neighbouring number is machine-extracted. `extract_bravura_outlines.py --anchors` now emits them from the pinned `bravura_metadata.json`, so the table regains them generated rather than remembered; the metadata's SHA-256 is deliberately unpinned and the script refuses until an operator with the font pins it. `GlyphCatalogIdentity` moves once, now, while no conformance claim declares the old one; user "delete them + teach the extractor") |
|
||
|
||
## Batch 3 — OPEN (2026-07-09)
|
||
|
||
Three candidates, so the pass reopens per the house rule. P13-S3 arrived as a
|
||
**live incorrectness** — a Push-4a follow-up audit found that the spelling-set
|
||
chain introduced to make `TransposeInterval` undoable had only one writer, so
|
||
undo erased an ordinary `RespellPitch` on either side of it. It is resolved.
|
||
S1, S2, and S3 are resolved. S5 and S6 were filed 2026-07-22 by verifying the
|
||
two Push-4a audit claims that `epiphany-core/DECISIONS.md` had carried as
|
||
**unverified** through two passes; both were real, and both were Chapter 4
|
||
defects standing in front of Push 4b. Both are now resolved, closing the spec
|
||
half of Push 4b.
|
||
|
||
**Filed 2026-07-22, second round.** S7 is a **bookkeeping repair**: the id was
|
||
minted in `spec/PLAN_PUSH4B_TUNING.md` ("new; file it"), ratified as that plan's
|
||
Ruling C, and implemented — but never entered this ledger, so an id existed
|
||
outside the index that is supposed to be the index. S8 and S9 are new. S9 is
|
||
the third instance of one pattern in a single day and, notably, **S4 is itself
|
||
an instance of it**, which is what moved it from incident to candidate.
|
||
|
||
**Filed 2026-07-23, third round.** S10–S12 are three underdetermined Chapter 4
|
||
types standing in front of Push 4b tranche 3 (the schema-major-3 wire freeze) —
|
||
the same shape as S5/S7 before tranche 1. Scoping the accidental subtree that
|
||
`accidental_extensions` drags onto the wire surfaced a raw `f64` that the byte
|
||
layer cannot encode, a leaf type referenced but never defined, and a version type
|
||
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, and S9 remain open; S10–S12 are resolved on ratification; S13 is
|
||
**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.)
|
||
|
||
| Id | One-line statement | Filed in | Status |
|
||
|---|---|---|---|
|
||
| P13-S1 | **169 of core_spec's 207 `requirement` blocks carry no `\label`**, so no conformance claim can cite them. Chapter 4 (`Tuning Systems and Pitch Spaces`) is 9/9 unlabeled and Chapter 11 (`Determinism Contract`) is 15/15, but the gap is universal, not local: `Semantic Operations` 24/27, `The Score Graph` 22/28, `Pitch` 10/13. The requirements *are* normative and *are* implemented; they simply cannot be named. Every `req:*` label the repo cites was added ad hoc by the pass that needed it | this file | **resolved** (all 207 core_spec requirements labelled, suite 277/277; and the pass found that labelling alone was insufficient — no document *numbered* its requirements, so a `\label` bound to the enclosing section and 61 of 207 shared a rendered number, one shared by six. All six documents now carry a real counter. Locked by `requirement_labels.rs`) |
|
||
| P13-S2 | `cmn-24` was declared in the built-in pitch-space table as CMN with quarter-tone accidentals while `PitchSpacePosition::Cmn.alteration`, transposition, and `CmnChromatic` were all specified in fixed semitones. The specification now denominates both CMN alteration sites and interval chromatic steps in the enclosing space's chromatic layer, with `cmn-24`'s map fixed explicitly. Until Push 4b provides structural registry resolution, the core fails closed outside provable built-in `cmn-12` instead of applying guessed 12-chromatic arithmetic | `spec/PLAN_P13S2_CMN24.md`; `crates/epiphany-core/DECISIONS.md` | **resolved** (schema and wire layout unchanged; `PitchSpaceUnavailable` maps to existing `PitchSpaceMismatch` 6; Operation Catalog 0.9.0; registry-backed generalization remains a Push 4b implementation blocker) |
|
||
| P13-S3 | The `engraved_spelling_chain` introduced with `TransposeInterval`'s undo had a **single writer**. `RespellPitch` mutates the same graph attachments but recorded only on `respell_chain`, so (a) undoing a transposed transaction after a prior respell restored the pitch and **erased the respell**, and (b) a respell landing canonically *after* the transaction was invisible to the chain, so a `StrictInverse` undo reported `Applied` and **wiped the newer authoring** instead of refusing as superseded. A `BestEffort` undo could also restore the pre-transpose pitch while leaving a spelling authored against the transposed one | `crates/epiphany-ops/DECISIONS.md` (P13-S3) | **resolved** (both operations now record on the shared key; pitch value + spelling set undo as one unit; new `req:opcat:spelling-set-chain`. The chain stays *physically* separate from `respell_chain`, which is `RespellPitch`'s LWW conflict state — folding transposes in would make a concurrent respell conflict with a transpose and move the canonical bytes of every existing history) |
|
||
| P13-S4 | **No labelled requirement governs vertical-band *heights*.** Pass 12 twice recorded a behavioural fix to the inter-staff solve — realizing an `InterStaffGap` band's declared `preferred_height` rather than a constructor default — and both times cited `req:layoutir:vertical-bands`, which never existed. The two real band requirements (`req:layoutir:primitive-band-ownership`, `req:layoutir:resolved-band-ownership`) govern *ownership*: which band a primitive belongs to, and that a resolved primitive retains it. Nothing states what a band's height means or that the solver must realize it, so the shipped behaviour is unspecified and the log invented a name for the gap | this file | **open** (found in P13-S1 review: an agent 'corrected' the dangling citations to the two ownership requirements, which replaced a visibly broken pointer with a silently wrong one) |
|
||
| P13-S5 | **The JI prime basis is specified at two lengths.** `req:pitch:ji-vector-basis` states that the built-in JI pitch spaces "order primes ascending **starting with 2**" and that `components.len()` MUST equal the basis size; the built-in pitch-space table calls `ji-5limit` "Two-dimensional (prime axes 3, 5)", `ji-7limit` three-dimensional and `ji-11limit` four-dimensional — each **exactly one short**, consistently, because the table is octave-reduced and the requirement is full-register. `req:tuning:builtin-tuning-catalog` makes the table normative, so a 5-limit vector is required to be both length 2 and length 3. The requirement's octave-reduction clause does not reconcile them: it *normalizes* the first component to a canonical range, it does not remove it | this file (verified 2026-07-22 against `core_spec.tex:1005-1008` and `:3496-3501`) | **resolved** (Push 4b Wave 1a, 5dcaa58: ratified **full register**. The three table rows gain prime 2 and become three-, four- and five-dimensional; `req:pitch:ji-vector-basis` is untouched, being the side that was already correct. A `JiVector` is an absolute *position*: without the prime-2 exponent, `ji-5limit` cannot distinguish C4 from C5) |
|
||
| P13-S6 | **No built-in tuning system's resolution is pinned to a versioned definition, and 14 of 20 have no definition at all.** `req:tuning:builtin-tuning-catalog` makes all 20 identifiers MUST-resolve with normative semantics, but only the six `tet-*` entries are actually specified — by `TuningResolution::EqualTemperament`'s structural rule. The other 14 are names: 3 meantone variants, `werckmeister-iii`/`iv`, `vallotti`, `kirnberger-ii`/`iii`, `young-ii`, `pythagorean` (the 3:2 ratio is named but not the fifth-chain construction or wolf placement), 3 `ji-static-5limit-*`, and `ji-adaptive-5limit`. `TuningResolution::Function` delegates them to a `TuningFunctionId`, which Chapter 10 lists as an **extension point**; no built-in is mapped to a function id and no function id is pinned. Against `req:tuning:tuning-resolution-determinism` — determinism MUST hold *across platforms* — two conforming implementations may each choose a different published Werckmeister III and both pass | `spec/DRAFT_P13S6_TEMPERAMENTS.md`; `spec/CONTRACT_P13S6_TEMPERAMENTS.md`; `spec/CONTRACT_P13S6_PROMOTION.md` | **resolved** (5e465a1: all 20 pinned by construction, not by cents table. The ten temperaments carry which fifths are tempered by what fraction of **which** comma, wolf placement where the name does not force it, exact ratios, and a closure sum that makes the section self-verifying. The static 5-limit systems are the lattice block $\{3^a5^b \mid a\in[-1,2], b\in[-1,1]\}$ — a 4×3 rectangle filling all twelve chromatic positions with nothing discarded — at three anchors, `req:tuning:ji-static-construction`. `ji-adaptive-5limit` is `"default-v1"`, the key-anchored static scale, with the anchor pinned to a staff and a moment (`req:tuning:adaptive-default-version`, `req:tuning:adaptive-anchor-derivation`). **Sourcing alone was insufficient**: a first draft produced two arithmetically impossible temperaments and one false ambiguity, every one properly cited, and only the closure invariant caught them — the contract gained a permanent "check the arithmetic, not just the source" section as a result) |
|
||
| P13-S7 | **`ScalePosition` referenced a score pitch-space registry that does not exist.** The listing comment said the space "References an entry in **the score's pitch-space registry**", and `req:pitch:default-pitch-space` said every score MUST **define** at least one pitch space and MAY define more. No such registry exists anywhere: `Score` has thirteen fields and none is one, and `ScoreTuningContext` carries ids plus accidental extensions — never space or system definitions. So the specification required an act (defining a pitch space) that the data model gives a score no way to perform | `spec/PLAN_PUSH4B_TUNING.md` (Ruling C); `spec/CONTRACT_PUSH4B_SPEC1.md` | **resolved** (Wave 1a, 5dcaa58: ratified **the catalog is closed**. Both "define"s become *select*, from the built-in catalog; the `ScalePosition` comment names the built-in catalog; score-local definition is deferred to a later schema major, because the Chapter 4 type surface has never had a consumer and `req:binfmt:frozen-layout` would freeze it permanently on first encode. **No struct gained a registry field — that was the point of the ruling.** Filed here retroactively: the id was minted in the plan, ratified, and implemented without ever entering this ledger) |
|
||
| P13-S8 | **Canonical state admits two encodings of one musical fact, and nothing normalizes them.** `TempoShape::Constant` (`core_spec.tex:2230`) says `end_tempo` MUST be `None` **or** equal to `start_tempo` — explicitly legalizing two spellings of an identical constant-tempo segment. `TempoSegment` encodes `end_tempo` positionally through `struct_codec!` (`codec.rs:1510`) and `TempoMap` is canonical state on `Score` (`graph.rs:1685`, `codec.rs:1517`), so the two forms produce different canonical bytes: **measured at 336 vs 363 for otherwise identical scores.** `invariants.rs:1335` merely permits both; no normalization runs anywhere. The project has ruled the other way everywhere else it looked — `req:determinism:unicode-canonicalization` applies NFC precisely so that "two texts whose NFC byte representations are equal are canonically equal", and `req:determinism:canonical-collection-order` pins iteration to a total order — but tempo has no equivalent, so two musically identical scores hash differently | this file (measured 2026-07-22 by encoding both forms) | **open** (ratification: normalize on encode to one form — `None` is the smaller and the one every generator already emits — or state that both are canonical and accept divergent hashes for identical music. Found sideways: the branch is also **untested**, since flipping `is_none_or` to `is_some_and` at `invariants.rs:1335` kills no test, so nothing in the suite constructs a `Constant` segment with `end_tempo: None`) |
|
||
| 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. **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) | **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 — but NOT amortised, as first written.** The ruling's "one accept-set raise, spent once" implied a single batch landing every new kind at `OperationEnvelopeBlock` major 3, spreading the raise across nine surfaces. Re-derived 2026-07-28 against the working tree, that is wrong: minimal stamping is a pure function of each payload's value, so the other eight surfaces stamp at major 0, 2, or 2 and never reach the raised bound at all. `SetTuningContext` is the sole payload born at v3, so the raise is charged to **exactly this surface** after all — which is why `spec/PLAN_GENESIS_OPS.md` §4 splits G2 and isolates it in **G2b**. **And the closure argument is not the canonical base:** the base is role-bound to major 0 (`mis_stamped_canonical_base`) and is a `MaterializedState`, which embeds no graph values for *any* field — including `metadata`, op-authored since M2d and durable purely through its operations. S13 closes on that precedent: the op log is canonical, and G2b makes an operation author the tuning context. Consequence to carry forward: once G2b lands, pruning would discard *authored* genesis state rather than merely re-derivable state, so the standing prohibition on pruning (blocked on disposition C) gains real teeth. 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) |
|
||
| P13-S14 | **The chunk schema *minor* is a normative MUST that no writer has ever honoured, so every appended operation discriminant is unattributable to version skew.** `binary_format.tex:2330` requires a writer to raise the chunk schema minor when it emits any discriminant appended after the minor it otherwise declares, with a stated rationale: so that a decode failure on an unknown appended discriminant is attributable to a version skew rather than to corruption. `SchemaVersion::for_major` (`bundle/src/ids.rs:204`) maps a major to a fixed constant — `V0` is `{0, 1}`, not `{0, 0}` (`ids.rs:173`), and `V1`/`V2`/`V3` carry minor 0 — and, decisively, **takes only a major**, so no per-kind additive minor can reach it — and **both** writer-side staging paths derive nothing but the major (`testkit/src/bundle_harness.rs:25`, `textproj/src/serialize.rs:183`, each mapping `max(OperationEnvelope::schema_major)` through `for_major`). So every kind appended after the initial vocabulary has no additive-version record: **24–27** (Phase-3 first tranche), **28–29** (the schema-major-2 repeat revision), **30** (Push 4a `TransposeInterval`), and **31** (genesis G1 `CreateInstrument`). The requirement's own failure mode is exactly what the gap produces: a reader meeting kind 31 from a newer writer cannot distinguish "my vocabulary is stale" from "these bytes are damaged" | this file (found 2026-07-28 during G2a contract review; verified against the working tree — `for_major`'s signature admits no minor, and neither staging path computes one) | **open, and deliberately extended.** Ruled 2026-07-28: policy ratified 2026-07-28 as a **global additive epoch with content-minimal stamping** (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4); the fix is its own rung, sequenced **after G2a and before G2b** — G2b appends kind 34, so running it first would either grow this sweep mid-flight or ship 34 with the very defect the rung retires. G2a appends kinds 32/33 and takes the violation from eight kinds to ten, knowingly — the debt is already eight deep, the +2 is immaterial against it, and one retroactive sweep over 24–33 is strictly less work than 24–31 now and 32–33 later. **What the rung owes:** a minor-assignment policy (which minor each past append belongs to — this is a retroactive judgement, not a derivation), a per-kind `schema_minor()` or equivalent, block minor = max over payloads, a `for_major` replacement that accepts one, and both staging paths. **Scoped 2026-07-28:** the minor IS in the chunk content-address preimage (`chunk_content_hash`, `chunk.rs:177`), so raising it moves every affected `ChunkId` and the manifest body naming it — real address churn, though **not** a `req:format:manifest-id` problem, since that promise is conditional on the same manifest body. It does **not** reach the text projection: op-block schemas are discarded during projection (`textproj/src/project.rs:424`), so no companion bump. And the problem is **not** per-`OperationKind`: an envelope also emits the outer `OperationPayload` discriminant (`ResolveEquivocation` = 3 carries no kind at all), so the gating work is an audit of every append-only vocabulary reachable from an affected payload. Note this is orthogonal to the *major* accept-set: `OperationEnvelopeBlock` stays at 2 through G2a and rises to 3 only at G2b |
|