T4 pin 7: the fixture recipe is approved, and the spike tree opens with it

Pin 7 of the spike contract holds that staff-by-measure dimensions do not pin a
workload, and the workload is what gets tessellated. This is that recipe: one
region, one staff instance per staff, a fixed seven-event bar of six eighths
plus a quarter, and integral per-measure quotas for rests, accidentals, ledger
notes and slurs. Nothing is sampled. The generator still takes a seed, but its
only role is replica minting as in the testkit fixtures -- content is fixed by
construction, which is strictly stronger than a fixed seed over a free
selection algorithm.

Three review rounds, and nearly every finding was the same mistake in a new
place: a claim about the pipeline inferred from an adjacent fact instead of
read from the code that emits.

Ties are not curves. The Curve doc says slurs engrave to one "and ties and
other span curves will follow" -- future tense; constrained.rs has a Slur arm
and no Tie arm, so ties trace as zero-extent anchors. The casting lines first
cited only transform curves produced upstream. Ties are now zero for the
primary ladder, and the decisive reason is not the miscount: every structural
anchor is emitted at the default x, which is the clef column, so casting files
all of them into the first system regardless of musical position. At the top
rung that would pile 2,400 anchors into the most likely damage target and
distort the per-system rebuild timing the whole spike turns on.

The census counts the parser's output, not the source string. H and V each
become a PathCommand::LineTo, so counting M/L/C/Z tokens undercounts exactly
the commands a tessellator consumes -- a sharp is 71, not 57. The census is now
defined as render_data(name).outline.len(), which measures the API candidates
actually see and removes the whole error class.

Ledger lines are emitted per staff step, not per off-staff note. A note one
space outside the staff emits none at all; the first ledger sits two steps out.
So off-staff pitches are pinned by absolute step rather than by "ledger
position", which would have produced about half the ledger ink it implied.

Also corrected: an all-eighths bar cannot yield a quarter rest (rest_glyph maps
value to glyph exactly), 4/4 uses timeSig4 which is single-subpath rather than
the multi-subpath coverage first claimed, barlines are glyphs and not strokes,
and the accidental cadence is one fixed cycle with bounded edge imbalance --
not equal thirds, since no rung's measure count divides by three.

Two-voice engraving is NOT unproven -- rs4_two_voice_counterpoint passes
against the real engraver. The narrower gap is collision handling, rest
displacement and deliberate stem-direction separation, and the ladder stays at
one voice as an isolation choice rather than out of doubt.

The remaining gate is separate and still open: no candidate code exists until
the generator emits its per-rung census, glyph histogram, SHA-256 of each
rung's canonical layout bytes, and the F4 engrave preflight -- and that output
is approved. Recipe approval is not generator approval.

Docs only. No Cargo manifest yet, so the root workspace is untouched; the
exclude line lands with the generator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
This commit is contained in:
Levi Neuwirth 2026-07-28 17:50:12 -04:00
parent a229a977a2
commit 42a8cc6dc5
1 changed files with 219 additions and 0 deletions

View File

@ -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<PathCommand>` 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 16 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.