# 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.