-
fix(surface): a button's ROLE decides its row — the remap had been assigning cells in reading order · 5089eec0
PLN found this by ear, twice in ten minutes, while recording OPAL takes: "techno drum mask is inverted on d8, should ALWAYS be the mask midiOff on ^60 and the ply or other multiplier/nassim button effect on the push-release 92" "drums in revolution seem way too fast ... yea def an inversion" ## The bug I shipped Phase 2 of the surface remap (5910aacc) column-aligned every button reference, and assigned each orbit's two cells in FIRST-APPEARANCE ORDER. That is role-blind, and tracks conventionally write the gesture line above the gate line: $ midiOn "^92" (ply "1 <2!3 4>") -- momentary flourish $ midiOff "^60" (mask "t(4,8,1)") -- latched gate so the first control encountered — the gesture — took the LATCHING cell, and the gate took the momentary one. Measured on the diff itself rather than on corpus history: of the button lines that commit moved, **32 were inverted**, 11 happened to be repaired, 10 were already wrong, 2 were right both times. 13 files, nearly all of them in the OPAL setlist. "Way too fast" is the audible signature: a `ply` on a latching button stays multiplied after one press. The mirror image is a `mask` that only gates while a finger holds it down. Neither errors. Neither is silent. Only the ear catches it — which is precisely the class of bug that needs a machine check. ## The convention, now written down once row E (41-44, 57-60), latching -> GATES (mask, struct) row F (73-76, 89-92), momentary -> GESTURES (ply, fast, stut, chop, slice...) A real limit of the grid, found while encoding it: **d1-d3 own only ONE button.** Their row-F cells (73,74,75) are the per-family mutes gMute1/2/3, so a gesture on ^41/^42/^43 is not an inversion — it is the only cell that exists. Every tool here skips those columns rather than inventing a slot. ## tools/button_roles.py — one judgement, three consumers migrate-columns.py ASSIGNS cells, fix-button-roles.py REPAIRS them, pvlint PV010 REPORTS them. Three copies of a musical rule is three chances to drift (the #97 lesson, applied before it could bite). Two parsing rules were earned in the space of one afternoon, and both were wrong in my first cut: 1. **A gate beats a gesture at the SAME level.** `mask "t(8,16,1)" . chop 16` is a latched break-gate that happens to chop while open. Treating it as ambiguous is what hid revolution's d8 pair from the first scan — the very pair PLN heard. 2. **But only at the same level.** `superimpose (struct "t . t(3,8)" . arpeggiate . (|+ note 12))` is a HELD FLOURISH; the `struct` builds the added layer's rhythm two levels down and says nothing about the button. A plain substring search called phunk's d6 a gate and moved it to the latching row — the same inversion, one level deeper. So the role belongs to the top-level chain, with nested arguments stripped and string literals skipped (`mask "t(4,8,1)"` would otherwise look like it opened two parens and swallowed the rest). 3. **The body is not the rest of the line.** `midiOn "^91" ( -- SLICE!` puts the function on the FOLLOWING lines. A same-line regex returns "unknown", and an unknown control gets a positional fallback — re-creating the inversion. The classifier now follows the parentheses, capped at 8 lines. ## Validation - 214 tests pass (24 new: 14 for the classifier, 10 for PV010/PV011). - The classifier is CONVERGENT: applying the repair twice yields 0 further rewrites. That is what caught mistakes 2 and 3 — each fix made the tool disagree with its own previous pass, and the disagreement was the bug report. - pvlint on the setlist: 0 errors, and PV010's 12 unfixable cases are reported as `info`, not warnings, because an orbit with two gestures genuinely cannot put both on the momentary row. A lint that nags about the impossible gets ignored, and this one has to stay trustworthy enough to gate a gig. ## PV011 — one FX bus slot shared by two orbits PLN's ask, right after PV004 surprised him with a shared cut group: "putain a unexpected cut shared, well done! like shared busses they screw things. can we lint and flag these? diff bus shared button is ok, but to be detected too". A `<fx>bus N` is a SLOT in SuperDirt, so two orbits naming the same number share one instance and the last event to land sets the amount for both. It immediately found take_5_drops putting d4's bass and d7's choir on `crushbus 41` — the standing suspect for "d7 feels way more attacky than before". Different effects on the same number are fine and are not flagged: the namespace is per effect, exactly as he said.
PLN (Algolia) authored5089eec0
×