Commit b8344ac1 by PLN (Algolia)

feat(pvlint): PV014 + PV011-as-error — and they immediately found mafia's acid

PLN, at rehearsal, on take_5_drops' d7 coming out muffled:

  "i saw also its crushbus was 41, a d7 with sub 4 is sus -- and two effects on
   same bus is a redflag as it means fighting for bus! so we gotta flag warning
   any 'bus nXY on dX' as sus (e.g. bus 41 (d4's) on d7 here), and any shared
   bus (^41 on d4 and d7) as PV error"

Two rules, exactly as specified.

PV014 (warning) — a bus slot numbered for a different orbit. The convention is
<orbit><slot>: d4 owns 41/42, d10 owns 101/102, and a bare `bus 4` on d4 is the
same idea with one slot. MEASURED before encoding it rather than assumed: 894
of 984 bus references under live/ conform, 90.9%. A real convention, not a
tidiness preference. Mis-numbering is *how you accidentally share* a bus, so
this rule sits upstream of PV011 and catches the cause, not the symptom.

PV011 promoted warning -> error. It shipped as a warning on the theory that
sharing a bus is legitimate glue. Rehearsal disproved that theory.

The asymmetry with PV008 is deliberate and now documented in both rules and
pinned by a test, so no future refactor quietly aligns them: yesterday PLN
DEMOTED a shared button to a warning ("its ok when two percs are linked"),
today he PROMOTED a shared bus to an error. A shared button is one hand doing
two things on purpose and you hear it instantly. A shared bus is two orbits
fighting over one effect instance with the winner decided by event density —
you don't hear a mistake, you hear a slightly wrong sound and blame the synth.
Surprises you can hear are warnings; surprises you cannot are errors.

THE PAYOFF, first run, in a track still in the set. mafia_sans_serif's d7 was
already carrying PLN's own note "FIXME ACID WEIRD NOT GOOD". It had THREE
collisions with d4's moog bass, none visible in the code:

    # cut 4        every acid note truncated the bass, and vice versa
    # crushbus 41  one crush instance, ^35 and ^32 fighting over it
    # octerbus 42  one octer instance, ^55 and ^52 fighting over it

d7 now owns cut 7 and slots 71/72. That is a diagnosis for a FIXME he wrote
without knowing the cause, produced by a rule written twenty minutes earlier.

Set is 0 errors / 9 warnings, silent-eval --seeded 14/14 ok, pvlint 68/68.
parent 71bb9bc2
...@@ -50,7 +50,15 @@ d5 $ gF2 $ gM3 -- ...@@ -50,7 +50,15 @@ d5 $ gF2 $ gM3 --
# cut 5 # cut 5
# gain 1.4 # gain 1.4
# squizbus 51 (range 0 2.5 "^33") # squizbus 51 (range 0 2.5 "^33")
d7 $ gF3 $ gM3 -- FIXME ACID WEIRD NOT GOOD d7 $ gF3 $ gM3 -- "FIXME ACID WEIRD NOT GOOD" — found it, 2026-08-02. THREE collisions
-- with d4's moog bass, all invisible in the code and all audible:
-- cut 4 -> every acid note truncated the bass and vice versa
-- crushbus 41 -> ONE crush instance, ^35 and ^32 fighting over it
-- octerbus 42 -> ONE octer instance, ^55 and ^52 fighting over it
-- Found by the new PV011(error)/PV014(warning) pair PLN asked for
-- after hearing the same shape in take_5_drops' d7. Now d7 owns
-- cut 7 and slots 71/72. TODO @PLN verify — the acid should stop
-- stuttering against the bass.
$ midiOn "^91" (ply 4) $ midiOn "^91" (ply 4)
-- $ slow 2 -- $ slow 2
$ note ( $ note (
...@@ -58,9 +66,9 @@ d7 $ gF3 $ gM3 -- FIXME ACID WEIRD NOT GOOD ...@@ -58,9 +66,9 @@ d7 $ gF3 $ gM3 -- FIXME ACID WEIRD NOT GOOD
+ (arp "up" "c'majPent'4") + (arp "up" "c'majPent'4")
) )
# "acidOto3091" # "acidOto3091"
# cut 4 # cut 7
# crushbus 41 (range 16 2.5 "^35") # crushbus 71 (range 16 2.5 "^35")
# octerbus 42 (range 0 2.5 "^55") # octerbus 72 (range 0 2.5 "^55")
# gain 1.1 # gain 1.1
-- FIXME(#54) d7 3rd+ effect: the grid gives d7 B7 + C7. Fold ^19 in via gSel rather than borrowing a column. -- FIXME(#54) d7 3rd+ effect: the grid gives d7 B7 + C7. Fold ^19 in via gSel rather than borrowing a column.
-- FIXME(#54) d7 drives more controls than the grid gives it (B7 + C7). ^19 now belongs to another orbit, so the line(s) below are COMMENTED OUT rather than left firing across columns. Nothing is lost — restore by removing the dashes once gSel folds this onto d7's own control. -- FIXME(#54) d7 drives more controls than the grid gives it (B7 + C7). ^19 now belongs to another orbit, so the line(s) below are COMMENTED OUT rather than left firing across columns. Nothing is lost — restore by removing the dashes once gSel folds this onto d7's own control.
......
...@@ -671,8 +671,19 @@ def pv011_fx_bus_shared(track: Track) -> Iterable[Finding]: ...@@ -671,8 +671,19 @@ def pv011_fx_bus_shared(track: Track) -> Iterable[Finding]:
knob. Different effects on the same NUMBER (`crushbus 41` vs `djfbus 41`) are knob. Different effects on the same NUMBER (`crushbus 41` vs `djfbus 41`) are
fine and are not flagged: the slot namespace is per effect. fine and are not flagged: the slot namespace is per effect.
A warning, not an error: sharing a bus is a legitimate way to glue two orbits PROMOTED TO ERROR 2026-08-02. It shipped as a warning on the theory that
into one colour. The point is that it should be a decision, not a surprise. sharing a bus is a legitimate way to glue two orbits into one colour. Then
take_5_drops' d7 came out muffled at rehearsal, PLN traced it to `crushbus
41` shared with d4, and ruled: *"two effects on same bus is a redflag as it
means fighting for bus! ... any shared bus (^41 on d4 and d7) as PV error"*.
Note this is the opposite call from PV008, where he demoted a shared BUTTON
to a warning ("its ok when two percs are linked"). The asymmetry is real and
worth stating: a shared button is one hand doing two things ON PURPOSE, and
you hear it immediately. A shared bus is two orbits fighting over one
effect instance, the winner decided by event density — you do not hear a
mistake, you hear a slightly wrong sound and blame the synth. Surprises you
can hear are warnings; surprises you cannot are errors.
""" """
owners: dict[tuple[str, int], list[tuple[int, int]]] = {} owners: dict[tuple[str, int], list[tuple[int, int]]] = {}
for orb in track.orbits(): for orb in track.orbits():
...@@ -691,7 +702,7 @@ def pv011_fx_bus_shared(track: Track) -> Iterable[Finding]: ...@@ -691,7 +702,7 @@ def pv011_fx_bus_shared(track: Track) -> Iterable[Finding]:
for _, line in sorted(first_line_of.items()): for _, line in sorted(first_line_of.items()):
yield Finding( yield Finding(
rule="PV011", rule="PV011",
severity="warning", severity="error",
line=line, line=line,
message=f"{fx}bus slot {num} is shared by {where}", message=f"{fx}bus slot {num} is shared by {where}",
detail=f"One {fx} instance serves both orbits, so the last event " detail=f"One {fx} instance serves both orbits, so the last event "
...@@ -804,6 +815,58 @@ def pv013_no_family_mute(track: Track) -> Iterable[Finding]: ...@@ -804,6 +815,58 @@ def pv013_no_family_mute(track: Track) -> Iterable[Finding]:
) )
# --------------------------------------------------------------------------
# PV014 — an FX bus slot numbered for a DIFFERENT orbit
# --------------------------------------------------------------------------
@rule
def pv014_fx_bus_wrong_column(track: Track) -> Iterable[Finding]:
"""`# crushbus 41` sitting on d7 — bus 41 is d4's slot.
PLN's ask, 2026-08-02, after take_5_drops' d7 came out muffled and he went
looking for why: *"i saw also its crushbus was 41, a d7 with sub 4 is sus --
and two effects on same bus is a redflag as it means fighting for bus! so we
gotta flag warning any 'bus nXY on dX' as sus (e.g. bus 41 (d4's) on d7
here)"*.
The corpus convention is `<orbit><slot>`: d4 owns 41/42, d5 owns 51/52, d10
owns 101/102, and the bare `bus 4` on d4 is the same idea with one slot.
Measured before encoding it — 894 of 984 bus references in live/ conform
(90.9%) — so this is a real convention, not a tidiness preference.
Why it matters beyond neatness: a mis-numbered bus is how you ACCIDENTALLY
share one. Nothing stops d7 from writing to 41; it just quietly joins d4 in
the same SuperDirt slot, and then PV011 fires — or worse, doesn't, because
the other orbit lives in a different track you loaded a minute ago. Naming
the slot after the orbit makes the collision impossible to author by mistake.
A WARNING: sharing is sometimes deliberate glue (see PV011), and a couple of
tracks legitimately reach across. The point is to be asked.
"""
for orb in track.orbits():
for ln_off, ln in enumerate(orb.lines):
for m in FXBUS_RE.finditer(strip_comment(ln)):
fx, num = m.group(1), int(m.group(2))
if num == orb.number or num // 10 == orb.number:
continue
owner = num // 10 if num >= 10 else num
yield Finding(
rule="PV014",
severity="warning",
line=orb.start + ln_off,
message=f"d{orb.number} uses {fx}bus {num}, but slot {num} "
f"is d{owner}'s by the <orbit><slot> convention",
detail=f"Renumber to {orb.number}1/{orb.number}2 so the slot "
f"is d{orb.number}'s alone. Left as-is, d{orb.number} "
f"and d{owner} share one {fx} instance the moment both "
f"play — the knob you turn for one is heard on the "
f"other, and which wins depends on event density "
f"(that is PV011). Deliberate glue? Say so in a "
f"comment and this stays a warning you have read.",
)
def check(track: Track, enabled: set[str] | None = None) -> list[Finding]: def check(track: Track, enabled: set[str] | None = None) -> list[Finding]:
out: list[Finding] = [] out: list[Finding] = []
for fn in RULES: for fn in RULES:
......
...@@ -392,10 +392,27 @@ def test_pv011_flags_one_fx_slot_shared_by_two_orbits(): ...@@ -392,10 +392,27 @@ def test_pv011_flags_one_fx_slot_shared_by_two_orbits():
'd7 $ n "0" # crushbus 41 (range 16 3.5 "^55")\n') 'd7 $ n "0" # crushbus 41 (range 16 3.5 "^55")\n')
f = lint(src, "PV011") f = lint(src, "PV011")
assert len(f) == 2 # one per orbit needing an edit assert len(f) == 2 # one per orbit needing an edit
assert all(x.severity == "warning" for x in f)
assert "crushbus slot 41" in f[0].message and "d4, d7" in f[0].message assert "crushbus slot 41" in f[0].message and "d4, d7" in f[0].message
def test_pv011_is_an_error_because_you_cannot_hear_the_mistake():
"""PLN 2026-08-02, having just played the exact case in this file's fixture.
He heard d7 come out muffled in take_5_drops, went looking, and found it
sharing crushbus 41 with the bass on d4:
*"two effects on same bus is a redflag as it means fighting for bus! ...
any shared bus (^41 on d4 and d7) as PV error"*
Deliberately the OPPOSITE call from PV008, where he demoted a shared button
to a warning. Both tests exist so a future refactor cannot quietly align
them: a shared button is audible intent, a shared bus is an inaudible bug.
"""
src = ('d4 $ n "0" # crushbus 41 (range 16 3.5 "^52")\n\n'
'd7 $ n "0" # crushbus 41 (range 16 3.5 "^55")\n')
assert all(x.severity == "error" for x in lint(src, "PV011"))
def test_pv011_different_effects_may_reuse_a_number(): def test_pv011_different_effects_may_reuse_a_number():
"""The slot namespace is per effect — PLN: "diff bus shared button is ok".""" """The slot namespace is per effect — PLN: "diff bus shared button is ok"."""
src = ('d4 $ n "0" # crushbus 41 (range 16 3.5 "^52")\n\n' src = ('d4 $ n "0" # crushbus 41 (range 16 3.5 "^52")\n\n'
...@@ -484,3 +501,33 @@ def test_pv013_a_commented_mute_does_not_count(): ...@@ -484,3 +501,33 @@ def test_pv013_a_commented_mute_does_not_count():
"""It is commented out, so the surface still cannot drop the orbit.""" """It is commented out, so the surface still cannot drop the orbit."""
f = lint('d8 $ gF1\n -- $ gM2\n', "PV013") f = lint('d8 $ gF1\n -- $ gM2\n', "PV013")
assert len(f) == 1 assert len(f) == 1
# ------------------------------------------------------------------- PV014
def test_pv014_flags_a_bus_slot_numbered_for_another_orbit():
"""PLN's exact example: "bus 41 (d4's) on d7 here"."""
f = lint('d7 $ n "0" # crushbus 41 (range 16 3.5 "^55")\n', "PV014")
assert len(f) == 1
assert f[0].severity == "warning"
assert "d7 uses crushbus 41" in f[0].message
assert "d4's" in f[0].message
def test_pv014_accepts_both_conforming_forms():
"""<orbit><slot> AND the bare one-slot form, which 894 refs in live/ use."""
assert lint('d4 $ n "0" # crushbus 41 (range 16 3.5 "^32")\n', "PV014") == []
assert lint('d4 $ n "0" # crushbus 42 (range 16 3.5 "^32")\n', "PV014") == []
assert lint('d4 $ n "0" # crushbus 4 (range 16 3.5 "^32")\n', "PV014") == []
def test_pv014_handles_two_digit_orbits():
"""d10 owns 101/102 — the rule must not read 101 as "d1's slot 01"."""
assert lint('d10 $ n "0" # crushbus 101 (range 16 3.5 "^18")\n', "PV014") == []
f = lint('d11 $ n "0" # crushbus 101 (range 16 3.5 "^18")\n', "PV014")
assert len(f) == 1 and "d10's" in f[0].message
def test_pv014_ignores_comments():
"""A commented-out bus is not a bus — the take_5_drops d7 case PLN parked."""
assert lint('d7 $ n "0"\n -- # crushbus 41 (range 16 3.5 "^55")\n', "PV014") == []
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment