epiphany/spec/PASS13_CANDIDATES.md

24 KiB
Raw Blame History

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. S10S12 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; S10S12 are resolved on ratification. The pass stays open.

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 AccidentalDefinitionScoreAccidentalExtensionsScoreTuningContext. req:determinism:canonical-floating-point requires canonical stored floats to be finite IEEE 754 binary64, and the byte layer enforces it with no escape hatch: serialize.rs:110 decodes floats only through CanonicalF64::from_le_bytes → NonFiniteFloat, so there is no Codec for f64. A raw f64 is therefore not merely risky in canonical state — it is unencodable without inventing a new unvalidated codec. The same chapter already resolved this exact tension forty lines later: EngravingBoundingBox (:3150) carries SpaceUnit edges with a rationale (:3191) invoking the same requirement this file (verified 2026-07-23 against core_spec.tex:3112, :3150-3197, and crates/epiphany-determinism/src/serialize.rs:110) resolved (ratified: Cents(CanonicalF64). A one-line spec correction, the only shape consistent with the existing byte layer — the identical maneuver Ruling D applied to the bounding box. Freezes in tranche 3b)
P13-S11 AnchorPoint, referenced by AccidentalEngraving.anchor, is defined nowhere. core_spec.tex:3166 names pub anchor: AnchorPoint; no struct or enum of that name exists in the spec, and epiphany-core (whose Cargo.toml has no epiphany-layout-ir dependency) cannot borrow any layout-ir type even if one shared the name — the same core-native requirement that forced EngravingBoundingBox. An undefined leaf frozen onto the wire is the KeyContext-shaped gap. Compounding it: the bounding box is documented "relative to the glyph's anchor point" (:3160), so freezing an anchor with no defined coordinate frame freezes a point with an undefined origin this file (verified 2026-07-23; AnchorPoint appears once in core_spec.tex, defined nowhere; epiphany-core/Cargo.toml lists no layout-ir) resolved (ratified: core-native AnchorPoint { x: SpaceUnit, y: SpaceUnit }, over the same SpaceUnit as advance_width and EngravingBoundingBox. Plus one normative sentence pinning the frame: x/y in canonical space units, y-up, relative to the glyph's coordinate origin — matching the repo's existing Bravura outline convention — so the anchor and the box it anchors share an unambiguous origin. Freezes in tranche 3b)
P13-S12 SmuflVersion is undefined, and its obvious representation orders SMuFL's real history backwards. SmuflVersionRequirement (core_spec.tex:3269) carries minimum/authored_against of type SmuflVersion, which Chapter 4 references but does not define (it exists only as a Chapter 7 / layout-ir type, glyph.rs:29, with literal-minor encoding — see the resolution). Ordering is load-bearing (SmuflVersionRequirement.minimum, :3271, gates the fallback of req:tuning:smufl-version-fallback), and the type is dual-purpose — it also anchors Chapter 9's GlyphCatalog::smufl_version() / GlyphCatalogIdentity (:10420, :10460), so this freeze touches layout-conformance identity, not just tuning. The trap: SMuFL versions are decimal fractions — 1.12 (2015), 1.18, 1.20 (2016), 1.3 (2019), 1.4 (2021) — that succeed by fraction (0.12 < 0.18 < 0.20 < 0.30 < 0.40). A { major: u16, minor: u16 } storing literal digits with derived Ord orders the minors 3 < 4 < 12 < 18 < 20, placing 1.3 and 1.4 before 1.12; old fonts declaring 1.18-era versions exist forever this file (verified 2026-07-23 against core_spec.tex:3269-3274 and SMuFL's published release history) resolved (ratified shape SmuflVersion { major: u16, minor_centi: u16 }, literal-minor storage rejected: the minor is stored fraction-normalized to hundredths — 1.12→(1,12), 1.18→(1,18), 1.20→(1,20), 1.3→(1,30), 1.4→(1,40), rule normative, release table a note. Derived Ord is then correct across the whole real history and collapses the 1.2/1.20 ambiguity (both → (1,20)). Correction on filing: the leaf is NOT undefined — it exists in epiphany-layout-ir/src/glyph.rs:29 as SmuflVersion { major, minor } with LITERAL minor ({1,4}) and derived Ord, so the backwards-ordering bug is LIVE there today (1.3 < 1.12), and it is a direct field of GlyphCatalogIdentity — conformance identity. epiphany-core cannot depend on layout-ir, and SmuflVersionRequirement is core, so the type MUST be defined in core and layout-ir must reuse it — a unification, not a fresh definition, which moves GlyphCatalogIdentity ({1,4}minor_centi 40). Tranche 3a defines core::SmuflVersion for the tuning use and leaves layout-ir's alone (a bounded, core-invisible homonym since core can't import layout-ir's); tranche 3b performs the unification and the deliberate GlyphCatalogIdentity move with golden/vector regen. The hundredths scale is blocking for 3b, free to adjust in 3a)