diff --git a/spikes/editor-toolkit/FIXTURE_RECIPE.md b/spikes/editor-toolkit/FIXTURE_RECIPE.md new file mode 100644 index 0000000..05a8cb3 --- /dev/null +++ b/spikes/editor-toolkit/FIXTURE_RECIPE.md @@ -0,0 +1,219 @@ +# T4 spike fixture recipe (pin 7) + +**Approved 2026-07-28** at the recipe level; moved here from +`spec/DRAFT_T4_FIXTURE_RECIPE.md`. Revision 1 asserted three things about the +pipeline that reading it disproves (ties as curves, two rest glyphs from +eighth-density, multi-subpath coverage from 4/4). Revision 2 fixed those and +introduced three of its own: a command census that skipped `H`/`V`, barlines +filed as strokes, and ledger quotas expressed in "positions" that can mean zero +ink. This revision corrects those and drops ties to zero. + +`CONTRACT_EDITOR_T4_SPIKE.md` pin 7 requires this recipe to be committed and +**user-reviewed before any candidate-specific implementation begins**, because +staff × measure dimensions do not pin the workload and the workload is what +gets tessellated. That review is discharged; the generator's census (below) is +a separate, still-open gate. + +**One recipe, scaled across rungs by dimension only.** F1 1 staff × 10 +measures, F2 4 × 32, F3 12 × 100, F4 24 × 200. Every rate is an integral +per-measure quota under one fixed cadence, so scale is the only thing that +moves between rungs — up to bounded edge effects where a rung's measure count +does not divide the cadence, which the census reports rather than the recipe +glossing. + +--- + +## What the pipeline actually draws + +Verified in `epiphany-layout-ir` and `epiphany-engrave` before choosing any +rate, because a recipe that asks for ink the pipeline does not emit silently +under-delivers. + +* **Curves are slurs, and only slurs.** `Curve`'s own doc: "Slurs engrave to + one of these; **ties and other span curves will follow**" + (`constrained.rs:109-111`) — future tense. `constrained.rs` has a + `TypedObjectId::Slur` arm and **no `Tie` arm**; ties fall through with the + other structures, which "are carried as zero-extent traced [anchors]" + (`constrained.rs:734`). Revision 1 counted ties as curves and inflated every + curve figure by 50%. The casting lines I cited only *transform* curves + already produced upstream — they were evidence of nothing. +* **Rest glyph follows note value exactly** — `rest_glyph` + (`engrave_theory.rs:112`): Whole→`restWhole`, Half→`restHalf`, + Quarter→`restQuarter`, Eighth→`rest8th`, and **`None` for anything shorter**, + deliberately, so missing coverage surfaces instead of misrendering. An + all-eighths recipe therefore yields `rest8th` **only**; revision 1 claimed + both quarter and eighth rests from a rhythm that cannot produce them. +* **`timeSig4` is single-subpath** (measured: 1 subpath, 34 commands). Revision + 1's claim that 4/4 brings multi-subpath coverage "free" was simply false. + `timeSig8` is the multi-subpath digit (3 subpaths) and 4/4 does not use it. +* **Beams are NOT drawn.** `beam_slope_penalty` is "vacuous 0.0: no beam + geometry is drawn yet (beams exist logically, not visually)" + (`quality.rs:40-41`). Eighth density here is an *event-count* choice, not a + beam-ink claim. When beams land, the stroke mix changes and this is revisited. +* **Strokes** — staff lines, stems, ledger lines, plus zero-extent traced + anchors (`casting.rs:1081`, `lib.rs:119`). **Barlines are glyphs, not + strokes** (`constrained.rs:1455`) — revision 2 listed them wrongly. +* **Ledger lines are emitted per staff step, not per off-staff note** + (`ledger_steps`, `constrained.rs:2642-2659`): the even steps strictly outside + the staff, from the staff out to the note. By absolute staff step: + **−1 and +9 emit 0 ledgers; −2, −3, +10 and +11 emit 1; −4 and +12 emit 2.** + The recipe therefore pins off-staff pitches by **exact staff step**; "±1 and + ±2 ledger positions" (revision 2) would have produced roughly half the ledger + ink it implied. +* **Clefs are emitted per staff instance**, not per system (no restatement on + later systems — `editor-gui/DECISIONS.md`), which holds given the topology + pinned below. + +### Measured glyph cost, since accidental *rate* does not control complexity + +**The census definition is `BravuraGlyphCatalog::render_data(name).outline.len()`** +— the typed `Vec` candidates actually consume. Revision 2 counted +`M`/`L`/`C`/`Z` tokens in the source `d` string and was wrong for it: the +parser converts each `H` and `V` to a `PathCommand::LineTo` +(`glyphs/src/path.rs:92`), so those are real commands in the tessellator's +input, not serialization shorthand. Recounted with them: + +| glyph | subpaths | commands | +|---|---|---| +| `accidentalSharp` | 2 | **71** | +| `accidentalDoubleSharp` | 1 | 37 | +| `accidentalNatural` | 2 | 29 | +| `accidentalFlat` | 2 | 24 | +| `noteheadBlack` | 1 | 6 | +| `restQuarter` | 1 | 38 | +| `rest8th` | 1 | 19 | +| `gClef` | 4 | 59 | +| `fClef` | 3 | 28 | +| `timeSig4` | 1 | 34 | + +A sharp costs **2.4×** a flat. So the accidental *mix* must be pinned, not just +the rate. + +--- + +## The recipe + +**Nothing is sampled.** Every quantity below is an integral per-measure quota +placed by a fixed rule, so the content is identical run to run, machine to +machine, with no rounding policy needed. The generator still takes a seed, but +its only role is replica/identity minting as in the testkit fixtures — **it +does not influence content**. That is strictly stronger than revision 1's +"fixed seed", which left the selection algorithm free. + +### Topology + +* **One region**, containing **one staff instance per staff**. No nested or + parallel regions. +* Staves take **`gClef` on even indices, `fClef` on odd** — both complex + multi-subpath clefs, in a mix a real keyboard/orchestral score would have. +* **4/4** throughout: `timeSig4` twice per staff (numerator + denominator). +* **Single-pitch note events only — no chords.** Chord cardinality would + change notehead-per-slot density and stem sharing, and is not a variable this + comparison needs. +* **One voice per staff instance** (see the limitation below). + +### Per measure, per staff — a fixed 7-event bar + +Rhythm: **six eighths + one quarter** = 6/8 + 2/8 = 4/4 exactly. Seven events, +metrically valid, and it makes both rest glyphs reachable. + +| Quota | Value | Placement rule | +|---|---|---| +| Events | **7** | positions 1–6 eighths, position 7 quarter | +| Rests | **1** | **even measures: the quarter (→ `restQuarter`); odd measures: eighth position 3 (→ `rest8th`)** — an exact 50/50 split of the two bundled rest glyphs | +| Accidentals | **1** (of 6 pitched events = **16.7%**, the integral replacement for revision 1's 15%) | on pitched event position 2; glyph cycles **sharp → flat → natural** by `measure index mod 3`. `accidentalDoubleSharp` excluded: rare in real music and single-subpath, so it adds cost without adding hole coverage | +| Ledger-bearing notes | **1** | on pitched event position 5; staff step cycles **−2 → −4 → +10 → +12** by `measure index mod 4`, giving ledger-stroke counts **1, 2, 1, 2** — an average of **1.5 ledger strokes per measure**, pinned by step rather than by a "ledger position" that can mean zero ink | +| Slurs | **1** | spanning positions 1→4, **within the measure** — never crossing a barline or system break, since a slur whose endpoints do not resolve to one staff traces instead of producing a curve (`constrained.rs:1605`) | +| Ties | **0** | see below | + +**Ties are zero for the primary ladder**, on the review's reasoning, which is +better than my revision-2 argument for keeping them. Two independent grounds: +the rule I wrote tied positions 6→7 while position 7 *is* the rest on even +measures, which is an **invalid tie pairing** +(`epiphany-core/src/invariants.rs:2101`); and — decisively — every structural +tie anchor is emitted at the **default x, which is the clef column** +(`constrained.rs:1155`, `default_x = region_x + CLEF_X`), so casting assigns +them all to the **first system** regardless of their musical measure. At F4 +that would pile 2,400 anchors into the most likely damage target and distort +per-system rebuild timing — corrupting the measurement the whole spike turns +on. Revisit when ties draw real geometry. + +**Pitch placement** is deterministic and quota-respecting: two ordered pitch +lists — *on-staff* (the staff's own span) and *off-staff* (the four steps +listed above) — each advanced cyclically. The designated ledger event draws from the +off-staff list; every other pitched event draws from the on-staff list. Pitch +never depends on the seed. + +**One fixed cadence, with bounded edge imbalance — not exact thirds.** 10, 32, +100, and 200 are none of them divisible by 3, so the sharp/flat/natural cycle +leaves at most one extra of one or two glyph types per staff; and **F1 has a +single staff, so it is `gClef` only** while every larger rung alternates +clefs. The cadence is identical and deterministic at every rung; the resulting +*counts* differ slightly at the edges, and the census reports what they are +rather than the recipe claiming they are equal. + +--- + +## What that produces + +Events and quotas are exact. Primitive counts are **estimates to be replaced by +the generator's measured census** (below) — pin 7 requires glyphs / strokes / +curves separately plus total path-command count. + +| Rung | staves × measures | events | pitched | **curves (slurs)** | accidentals | ledger strokes | +|---|---|---|---|---|---|---| +| F1 | 1 × 10 | 70 | 60 | **10** | 10 | 15 | +| F2 | 4 × 32 | 896 | 768 | **128** | 128 | 192 | +| F3 | 12 × 100 | 8,400 | 7,200 | **1,200** | 1,200 | 1,800 | +| F4 | 24 × 200 | 33,600 | 28,800 | **4,800** | 4,800 | 7,200 | + +Plus stems (≈ one per notehead), staff lines (5 × staves × systems), and +barline **glyphs**. + +--- + +## Approve the generator, not just the recipe + +Per the review, and I agree it is the right gate: the generator lands **before +any candidate code**, and emits a **per-rung census** — per-type primitive +counts, total path commands, and a glyph-name histogram — plus a **`SHA-256` +hash of each rung's `ResolvedLayoutIR` canonical bytes**, the algorithm named +here rather than left to the implementation. That census is what you approve; +the numbers above are my arithmetic, and the census is the measurement. The +hash then makes every later round's fixture provably the one that was approved. + +**This is the standing gate: no candidate code exists until that output is +approved.** Recipe approval is not generator approval. + +**F4's engrave time is measured in that same preflight**, against the corrected +generator — not now, and not against an underspecified recipe, where a timing +could not settle anything. If F4 exceeds pin 7's ten-minute wall it is dropped +and the deciding rung becomes **the largest remaining engraving-valid rung** — +expected to be F3, but not unconditionally F3. + +--- + +## Stated limitation: one voice + +**Corrected from revision 1, which overstated the gap.** Two-voice engraving is +*not* unproven: `rs4_two_voice_counterpoint_passes_minimal` +(`testkit/tests/reference_suite.rs:89`) exercises it against the real engraver +and passes, and logical projection iterates every voice. Revision 1 inferred a +gap from casting tests that happen to read `voices[0]` — evidence about those +tests, not about the engraver, which is the same reading-*a*-path-for-*the*-path +error that has bitten this track before. + +What is genuinely unverified is narrower: **collision handling between voices, +rest displacement, and deliberate stem-direction separation**. The deciding +ladder stays at one voice as an isolation choice — a spike measuring +tessellation should not also be exercising the engraver's least-proven +geometry — and a multi-voice engraving check belongs on its own, later. + +## What this recipe deliberately does not vary + +Dynamics, articulations, text, tuplets, grace notes, repeats. Several are +unengraved today (§3.7's registry), and the rest add model surface without +adding a distinct *primitive type* — the tessellator sees glyphs, strokes, and +curves, and this recipe's job is to load those three in a realistic ratio. +Text is the exception and is covered by round 2's synthetic `SpikeResolvedText` +fixtures, where it belongs.