Commit 3ab9ae03 by PLN (Algolia)

feat(orphan-orbits): name the ghosts before the stage does (#68)

PLN, by ear, 2026-07-28: loaded vague_de_crime, played bombe_dj, and "heard
diam's voice, now still hearing the synth from crimewave... i hear it regardless
of gains, but filters work on them".

THE MECHANISM, and why his two symptoms are the proof
`dN $ ...` replaces orbit N and says NOTHING about the others, so every orbit the
outgoing track declares and the incoming one does not just keeps running forever
underneath the new track. "Regardless of gains" follows because the new track's
gain lines address only ITS OWN orbits — nothing in it can reach the ghost.
"But filters work on them" follows because gF3 is the GLOBAL CC51 DJ filter,
applied INSIDE the ghost's own still-running pattern. An orbit that ignores the
faders but answers the DJ filter is diagnostic of exactly this, and nothing else.

Not a rig fault. It bites at every transition where the outgoing track declares
an orbit the incoming one does not — which is 8 of the 12 OPAL transitions.

WHAT SHIPPED
armada/setlist_opal2026.txt — the set as DATA, in play order, resolved from
backlog.md's codenames by locating each file rather than guessing ("Mafia" is
mafia_sans_serif, "Le shifteur marteau" is electric_hammer, "Take five Drops" is
take_5_drops). Reordering this file re-answers the ghost question, which is
exactly what #12 needs.

tools/orphan-orbits.py — reuses pvlint's existing orbit parser (DRY) and:
  * walks the set pairwise and names each transition's ghosts WITH THE SOUND they
    play, because "d6 (crimewave)" is actionable on stage and "d6" is not
  * --matrix, the full A->B grid, so #12 can order the set with the ghosts in view
  * --silence TRACK, a paste-ready `dN $ silence` preamble
  * --pair A B, exiting non-zero when ghosts exist so #44 can gate on it

THE RESULT FOR THIS SET
12 transitions, 8 leave ghosts, 16 orphaned orbits. Worst: after REVOLUTION (4),
after Gimme Acid (3). The original ear-report reproduces exactly —
vague_de_crime -> bombe_dj leaves {6, 10}, and d6 IS the crimewave synth.

And one ordering lever worth more than the report: Perfect <3 declares ALL TWELVE
orbits, so its column in the matrix is entirely clean. It is a free RESET POINT —
put it after the messiest stretch and ghosts stop propagating. The tool now
detects and names reset points generally rather than leaving that to be noticed.

WHY IT STOPS SHORT OF FIXING IT AUTOMATICALLY
The obvious automation is to have tidal-remote eval a generated silence block.
Read the plugin first: `eval-file` calls atom.workspace.open(..., activatePane:
true) — it OPENS AND ACTIVATES the file, so it would yank the editor away from
the track being played, mid-transition. That is worse than the ghost. Until the
plugin gets a focus-free `code` command, the fix is `hush` (safe under pressure,
costs the tail) or pasting the block at the top of a track — and that is a .tidal
edit, which is PLN's call, not mine. `dN` IS `xfade N`, so `d6 $ silence` should
FADE the ghost rather than cut it; that wants an ear before it is assumed.

TESTS: 39 new, suite 306 -> 345, all green.
The one that matters is the cross-validation: the parser's orbit set is asserted
against the TEN sets hand-measured on 2026-07-28, before this parser existed. All
ten agree, so the tool and the ear agree. Also pinned: a commented-out `-- d6`
counts as neither declared nor overwriting (correct in both directions); the
silence block never silences the track's own orbits (that shape is a mute-bomb
and would be blamed on the rig); it uses `silence` and never `# gain 0` (which
leaves the pattern scheduled and revivable by a later global gain); and d1-d5
never orphan anywhere in this set — so if a future track drops one, the ghost
lands on the loudest possible orbit and the suite says so before the stage does.
parent 0b23f3d9
# OPAL 2026 — the set, in play order.
#
# One track per line: <repo-relative .tidal path> # <codename> [<bpm>]
# Blank lines and #-comments ignored. Order matters: tools/orphan-orbits.py walks
# it pairwise to find which orbits ghost across each transition (#68), so
# reordering this file re-answers that question.
#
# Resolved from backlog.md's "## OPAL 2026" section (the canonical informal
# source), which lists codenames only. The mapping was verified by locating each
# file, not guessed: "Mafia" is mafia_sans_serif, "Sunshine" is you_my_sunshine,
# "Le shifteur marteau" is electric_hammer, "Take five Drops" is take_5_drops.
# Section headings from the backlog are kept as comments so the arc stays visible.
# --- Ouverture ---
live/midi/nova/techno/bombe_dj.tidal # Ceci n'est pas Une Bombe [124]
live/midi/nova/dnb/wap.tidal # WAP [133]
live/techno/do_it_right.tidal # Am i Doing it Right? [89]
# --- We call it NuJazz ---
live/midi/nova/lounge/take_5_drops.tidal # Take five Drops [124]
live/collab/raph/piment_bresilien.tidal # Piment Bresilien [124]
live/midi/nova/remix/perfect.tidal # Perfect <3 [128]
live/midi/nova/acid/gimme_acid.tidal # Gimme Acid??!? [160]
live/techno/vague_de_crime.tidal # Vague de CRIME [120]
live/collab/raph/mafia_sans_serif.tidal # Mafia [160]
live/midi/nova/dnb/liquid/you_my_sunshine.tidal # Sunshine [166]
# --- Finale ---
live/collab/raph/desire.tidal # Desire [129]
# --- Encore ---
live/midi/nova/jazz/the_revolution_will_be_sampled.tidal # REVOLUTION
live/midi/nova/techno/electric_hammer.tidal # Le shifteur marteau
#!/usr/bin/env python3
"""orphan-orbits — which orbits keep playing after you load the next track (#68).
THE MECHANISM (confirmed by ear, then by code — not a hypothesis)
PLN, 2026-07-28: loaded vague_de_crime, then played bombe_dj, and "heard diam's
voice, now still hearing the synth from crimewave... i hear it regardless of gains,
but filters work on them".
`dN $ ...` replaces orbit N and says NOTHING about the others. So every orbit the
outgoing track declares and the incoming one does not just keeps running, forever,
underneath the new track. vague_de_crime declares 1-8 and 10; bombe_dj declares
1-5, 7, 8, 9; so d6 and d10 survive — and d6 of vague_de_crime is literally the
crimewave synth he heard.
Both of his symptoms fall out of this, which is what confirms it:
* "regardless of gains" — the new track's gain lines address only ITS OWN orbits,
so nothing in it can turn the ghost down.
* "but filters work on them" — gF3 is the GLOBAL CC51 DJ filter, applied inside
the ghost's own still-running pattern, so it still filters. (An orbit that
ignores the faders but answers the DJ filter is diagnostic of exactly this.)
This is not a rig fault. It will bite at EVERY transition where the outgoing track
declares an orbit the incoming one does not.
WHAT THIS TOOL DOES
orphan-orbits.py walk the set in order; per transition, name the
orbits that will ghost, and what they sound like
orphan-orbits.py --matrix the full A->B matrix, so #12 can ORDER the set to
prefer low-orphan adjacencies
orphan-orbits.py --pair A B one transition
orphan-orbits.py --silence TRACK the paste-ready preamble that pre-silences every
orbit TRACK does not use
orphan-orbits.py --json
It reads only. No .tidal file is edited, and nothing is sent to the rig.
WHY IT DOES NOT JUST FIX IT FOR YOU
The obvious automation — have tidal-remote eval a generated `dN $ silence` block —
does not work live: the remote channel's `eval-file` OPENS AND ACTIVATES the file in
Pulsar, so it would steal the editor away from the track being played, mid-transition.
Until there is a focus-free `code` command in the plugin, the fix is one of:
1. `hush` between tracks. Zero code, safe under pressure, costs the tail.
2. Paste `--silence TRACK`'s block at the top of the track (a .tidal edit, PLN's
call). `dN` IS `xfade N`, so `d6 $ silence` should FADE the ghost rather than
cut it — verify that by ear before assuming a cut.
3. The musical answer: a real transition helper (#13).
VERIFY BY MEASUREMENT, not by reading this output:
boot track A, boot track B, then probe the orphans it names —
tools/probe-chain.py -d <orphans> -s 12 -t 1.5
signal on an orphan = the ghost is real; -inf on all = fixed.
(probe-chain makes ~18 PipeWire nodes per run and each costs Ardour an xrun.
Do NOT read those as a rig fault — feedback_observer_perturbs.)
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from pvlint.core import Track # noqa: E402 (the orbit parser already lives here)
REPO = Path(__file__).resolve().parent.parent
SETLIST = REPO / "armada" / "setlist_opal2026.txt"
# The rig has 14 orbits (reference_sc_boot). A silence preamble should cover the ones
# a track could plausibly leave behind, i.e. every orbit any track in the set uses.
MAX_ORBIT = 14
# What a ghost will SOUND like matters more than its number when you are on stage, so
# the report quotes the outgoing track's own line for each orphan.
SOUND_RE = re.compile(r'"([a-zA-Z_][\w]*)(?::\d+)?[^"]*"')
def load_setlist(path: Path = SETLIST) -> list[tuple[Path, str]]:
"""[(path, label)] in play order. Fails loudly on a missing file — a setlist
that silently drops a track would silently drop a transition."""
out: list[tuple[Path, str]] = []
if not path.exists():
raise SystemExit(f"orphan-orbits: no setlist at {path}")
for raw in path.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
rel, _, comment = line.partition("#")
p = REPO / rel.strip()
if not p.exists():
raise SystemExit(f"orphan-orbits: setlist names a missing track: {rel.strip()}")
out.append((p, comment.strip() or p.stem))
return out
def resolve(raw: str) -> Path:
p = Path(raw)
if p.exists():
return p
if (REPO / raw).exists():
return REPO / raw
name = raw if raw.endswith(".tidal") else raw + ".tidal"
hits = sorted((REPO / "live").rglob(f"*{name}")) + \
sorted((REPO / "copycat").rglob(f"*{name}"))
if len(hits) == 1:
return hits[0]
raise SystemExit(f"orphan-orbits: cannot resolve {raw!r} ({len(hits)} matches)")
def orbits_of(path: Path) -> set[int]:
"""The orbits a track DECLARES, i.e. the ones it will overwrite on load.
Commented-out declarations do not count, and that is right in both directions:
a `-- d6 $ ...` never plays (so it leaves no ghost), and it never overwrites
(so an inherited d6 survives it).
"""
return Track(path=str(path), text=path.read_text()).declared_orbits()
def sounds_of(path: Path) -> dict[int, str]:
"""orbit -> a short description of what it plays, for the report."""
out: dict[int, str] = {}
for orb in Track(path=str(path), text=path.read_text()).orbits():
names = SOUND_RE.findall(orb.code())
# Keep only plausible sample/synth NAMES. The regex also catches short quoted
# mini-notation and note tokens ("f", "q", "t", "a e"), which produced hints
# like `d7(FMRhodes1, q)` — noise in the one place the report has to be
# readable at a glance on stage.
picked = [n for n in names if len(n) >= 3 and not n.isdigit()][:2]
out.setdefault(orb.number, ", ".join(picked) or "?")
return out
def orphans(a: Path, b: Path) -> set[int]:
"""Orbits still playing from A after B is loaded."""
return orbits_of(a) - orbits_of(b)
def silence_block(path: Path, max_orbit: int = MAX_ORBIT,
used_by_set: set[int] | None = None) -> list[str]:
"""`dN $ silence` for every orbit this track does NOT declare.
Scoped to the orbits the SET actually uses when that is known, rather than all
14: silencing an orbit nobody ever touches is harmless but it is noise in the
file, and a shorter block is one PLN might actually paste. Same reasoning as
measuring the set rather than the corpus.
`silence`, never `# gain 0`: gain 0 leaves the pattern scheduled and still
consuming a voice, and a later global gain change can bring it back.
"""
mine = orbits_of(path)
pool = sorted(used_by_set) if used_by_set else list(range(1, max_orbit + 1))
return [f"d{n} $ silence" for n in pool if n not in mine]
def cmd_chain(setlist: list[tuple[Path, str]], as_json: bool = False) -> int:
rows = []
for (pa, la), (pb, lb) in zip(setlist, setlist[1:]):
orph = sorted(orphans(pa, pb))
rows.append({"from": str(pa.relative_to(REPO)), "from_label": la,
"to": str(pb.relative_to(REPO)), "to_label": lb,
"orphans": orph})
if as_json:
print(json.dumps(rows, indent=2))
return 0
total = sum(len(r["orphans"]) for r in rows)
dirty = [r for r in rows if r["orphans"]]
print("=" * 78)
print(" ORPHAN ORBITS ALONG THE SET (#68)")
print("=" * 78)
print(f" {len(rows)} transitions, {len(dirty)} of them leave ghosts, "
f"{total} orphaned orbits in total\n")
for r in rows:
arrow = "→"
if not r["orphans"]:
print(f" clean {r['from_label']:<26} {arrow} {r['to_label']}")
continue
snd = sounds_of(REPO / r["from"])
names = ", ".join(f"d{n}({snd.get(n, '?')})" for n in r["orphans"])
print(f" GHOST {r['from_label']:<26} {arrow} {r['to_label']}")
print(f" leaves {names}")
if dirty:
print("\n WORST OFFENDERS (tracks that leave the most behind them):")
by_from: dict[str, int] = {}
for r in dirty:
by_from[r["from_label"]] = by_from.get(r["from_label"], 0) + len(r["orphans"])
for label, n in sorted(by_from.items(), key=lambda kv: -kv[1])[:5]:
print(f" {n:>2} orbit(s) after {label}")
print("\n Under pressure: `hush` between these two. To fix it properly, see")
print(" --silence <the INCOMING track> and paste the block at its top.")
print("=" * 78)
return 0
def cmd_matrix(setlist: list[tuple[Path, str]]) -> int:
"""The full A->B matrix. #12 orders the set; this tells it which adjacencies
are cheap, so the ordering decision can be made with the ghosts in view."""
names = [lb.split("[")[0].strip()[:11] for _, lb in setlist]
orbs = [orbits_of(p) for p, _ in setlist]
print("\n rows = outgoing, cols = incoming. cell = how many orbits ghost.")
print(" '.' = clean transition.\n")
print(" " + " " * 13 + "".join(f"{i+1:>4}" for i in range(len(setlist))))
for i, nm in enumerate(names):
cells = []
for j in range(len(setlist)):
if i == j:
cells.append(" -")
else:
n = len(orbs[i] - orbs[j])
cells.append(" ." if n == 0 else f"{n:>4}")
print(f" {i+1:>2} {nm:<11}" + "".join(cells))
print("\n declared orbits per track:")
for i, (p, lb) in enumerate(setlist):
print(f" {i+1:>2} {lb.split('[')[0].strip():<24} "
f"{sorted(orbs[i])}")
# The two big emitters are worth naming outright.
worst = sorted(range(len(setlist)), key=lambda i: -len(orbs[i]))[:2]
print(f"\n Widest emitters: " + ", ".join(
f"{names[i]} ({len(orbs[i])} orbits)" for i in worst) +
" — almost anything after them inherits ghosts.")
# A track that declares a SUPERSET of everything else has an all-clean column:
# it overwrites every orbit, so it can follow anything without inheriting a
# ghost. That makes it a free RESET POINT, which is a real ordering lever for
# #12 — put one after the messiest stretch and the ghosts stop propagating.
union = set().union(*orbs) if orbs else set()
resets = [i for i in range(len(setlist))
if all(len(orbs[j] - orbs[i]) == 0 for j in range(len(setlist)) if j != i)]
if resets:
print(" RESET POINTS (clean to enter from anywhere — they declare every orbit")
print(" the rest of the set uses, so nothing survives into them):")
for i in resets:
print(f" {i+1:>2} {names[i]}")
else:
print(f" No track covers the set's full orbit span {sorted(union)}, so every")
print(" ordering leaves some ghosts somewhere — the block from --silence is the fix.")
return 0
def cmd_pair(a: str, b: str) -> int:
pa, pb = resolve(a), resolve(b)
orph = sorted(orphans(pa, pb))
print(f" {pa.name} → {pb.name}")
print(f" outgoing declares {sorted(orbits_of(pa))}")
print(f" incoming declares {sorted(orbits_of(pb))}")
if not orph:
print(" CLEAN — the incoming track overwrites everything the outgoing one used.")
return 0
snd = sounds_of(pa)
print(f" ORPHANS {orph}:")
for n in orph:
print(f" d{n:<3} {snd.get(n, '?')}")
print(f"\n probe it: tools/probe-chain.py -d {','.join(map(str, orph))} -s 12 -t 1.5")
return 1
def cmd_silence(track: str, scope_to_set: bool = True) -> int:
p = resolve(track)
used = None
if scope_to_set and SETLIST.exists():
used = set()
for sp, _ in load_setlist():
used |= orbits_of(sp)
block = silence_block(p, used_by_set=used)
print(f"-- orphan guard for {p.name} (generated by tools/orphan-orbits.py, #68)")
print(f"-- this track declares {sorted(orbits_of(p))}; the lines below silence every")
print("-- other orbit the set uses, so nothing from the PREVIOUS track survives.")
print("-- Paste as its own block at the TOP of the file. `dN` is `xfade N`, so this")
print("-- should fade the ghost out rather than cut it — confirm by ear.")
if not block:
print("-- (nothing to silence: this track already declares every orbit in the set)")
return 0
for line in block:
print(line)
return 0
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(
prog="orphan-orbits.py", description=__doc__.split("\n")[0],
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--setlist", type=Path, default=SETLIST)
ap.add_argument("--matrix", action="store_true", help="full A->B orphan matrix")
ap.add_argument("--pair", nargs=2, metavar=("FROM", "TO"))
ap.add_argument("--silence", metavar="TRACK")
ap.add_argument("--all-orbits", action="store_true",
help="with --silence: cover all 14 orbits, not just the set's")
ap.add_argument("--json", action="store_true")
a = ap.parse_args(argv)
if a.pair:
return cmd_pair(*a.pair)
if a.silence:
return cmd_silence(a.silence, scope_to_set=not a.all_orbits)
setlist = load_setlist(a.setlist)
if a.matrix:
return cmd_matrix(setlist)
return cmd_chain(setlist, as_json=a.json)
if __name__ == "__main__":
sys.exit(main())
"""Tests for tools/orphan-orbits.py — the ghost-orbit analysis (#68).
The invariant that matters: this tool tells PLN which orbits will still be audible
after a transition. A MISS is a ghost on stage he was told would not be there, so
the parse has to be conservative in the right direction, and the integration test
below is run against the REAL set rather than fixtures — a parser that agrees with
a synthetic file and disagrees with the corpus has proven nothing.
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
import pytest
TOOLS = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("orphan_orbits", TOOLS / "orphan-orbits.py")
oo = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(oo)
def _track(tmp_path: Path, body: str, name: str = "t.tidal") -> Path:
p = tmp_path / name
p.write_text(body)
return p
# --------------------------------------------------------------------------- #
# what counts as "declared"
# --------------------------------------------------------------------------- #
def test_declared_orbits_are_the_ones_that_will_be_overwritten(tmp_path):
p = _track(tmp_path, 'd1 $ s "bd"\n\nd7 $ s "hh"\n\nd10 $ s "arpy"\n')
assert oo.orbits_of(p) == {1, 7, 10}
def test_a_commented_out_orbit_is_not_declared(tmp_path):
"""Right in BOTH directions, which is why it is safe: a `-- d6` never plays so
it leaves no ghost, and it never overwrites so an inherited d6 survives it."""
p = _track(tmp_path, 'd1 $ s "bd"\n\n-- d6 $ s "ghost"\n')
assert oo.orbits_of(p) == {1}
def test_an_indented_continuation_line_is_not_a_new_orbit(tmp_path):
p = _track(tmp_path, 'd1 $ s "bd"\n # gain 1\n # room 0.3\n')
assert oo.orbits_of(p) == {1}
def test_two_digit_orbits_parse(tmp_path):
p = _track(tmp_path, 'd1 $ s "bd"\n\nd12 $ s "x"\n\nd14 $ s "y"\n')
assert oo.orbits_of(p) == {1, 12, 14}
# --------------------------------------------------------------------------- #
# the orphan set
# --------------------------------------------------------------------------- #
def test_orphans_are_what_the_outgoing_track_has_and_the_incoming_lacks(tmp_path):
a = _track(tmp_path, 'd1 $ s "a"\n\nd6 $ s "b"\n\nd10 $ s "c"\n', "a.tidal")
b = _track(tmp_path, 'd1 $ s "a"\n\nd9 $ s "d"\n', "b.tidal")
assert oo.orphans(a, b) == {6, 10}
def test_orphans_are_asymmetric(tmp_path):
"""A->B and B->A are different questions. An orbit the INCOMING track adds is
not a ghost — it is just a new sound."""
a = _track(tmp_path, 'd1 $ s "a"\n', "a.tidal")
b = _track(tmp_path, 'd1 $ s "a"\n\nd9 $ s "d"\n', "b.tidal")
assert oo.orphans(a, b) == set()
assert oo.orphans(b, a) == {9}
def test_a_superset_track_is_always_a_clean_destination(tmp_path):
a = _track(tmp_path, 'd1 $ s "a"\n\nd6 $ s "b"\n', "a.tidal")
b = _track(tmp_path, 'd1 $ s "a"\n\nd6 $ s "b"\n\nd9 $ s "c"\n', "b.tidal")
assert oo.orphans(a, b) == set()
def test_a_track_never_orphans_itself(tmp_path):
a = _track(tmp_path, 'd1 $ s "a"\n\nd6 $ s "b"\n', "a.tidal")
assert oo.orphans(a, a) == set()
# --------------------------------------------------------------------------- #
# the silence block
# --------------------------------------------------------------------------- #
def test_the_silence_block_covers_every_orbit_the_track_does_not_declare(tmp_path):
p = _track(tmp_path, 'd1 $ s "a"\n\nd3 $ s "b"\n')
block = oo.silence_block(p, used_by_set={1, 2, 3, 6, 10})
assert block == ["d2 $ silence", "d6 $ silence", "d10 $ silence"]
def test_the_silence_block_never_silences_an_orbit_the_track_uses(tmp_path):
"""Silencing the track's own orbit would mute the track it is meant to protect —
the mute-bomb shape, and it would be blamed on the rig."""
p = _track(tmp_path, "".join(f'd{n} $ s "x"\n\n' for n in (1, 4, 9)))
block = oo.silence_block(p, used_by_set=set(range(1, 15)))
for n in (1, 4, 9):
assert f"d{n} $ silence" not in block
def test_the_silence_block_uses_silence_and_never_gain_zero(tmp_path):
"""`# gain 0` leaves the pattern scheduled and still burning a voice, and a
later global gain change can bring the ghost back from the dead."""
p = _track(tmp_path, 'd1 $ s "a"\n')
block = oo.silence_block(p, used_by_set={1, 2})
assert all(ln.endswith("$ silence") for ln in block)
assert not any("gain" in ln for ln in block)
def test_a_track_declaring_everything_needs_no_silence_block(tmp_path):
p = _track(tmp_path, "".join(f'd{n} $ s "x"\n\n' for n in range(1, 5)))
assert oo.silence_block(p, used_by_set={1, 2, 3, 4}) == []
def test_the_silence_block_defaults_to_the_full_orbit_span(tmp_path):
p = _track(tmp_path, 'd1 $ s "a"\n')
block = oo.silence_block(p)
assert len(block) == oo.MAX_ORBIT - 1
assert "d14 $ silence" in block
def test_the_silence_block_is_scoped_to_the_set_when_known(tmp_path):
"""Shorter blocks get pasted; 13 lines of silence for orbits nobody uses do not.
Same reasoning as measuring the set rather than the corpus."""
p = _track(tmp_path, 'd1 $ s "a"\n')
assert len(oo.silence_block(p, used_by_set={1, 6})) == 1
# --------------------------------------------------------------------------- #
# sound hints — the report has to be readable at a glance ON STAGE
# --------------------------------------------------------------------------- #
def test_sound_hints_name_the_sample(tmp_path):
p = _track(tmp_path, 'd6 $ slice 4 "<0 1>" $ "crimewave:15" # cut 9\n')
assert "crimewave" in oo.sounds_of(p)[6]
def test_sound_hints_drop_short_mininotation_tokens(tmp_path):
"""The regression: hints came out as `d7(FMRhodes1, q)` and `d8(t,
jungle_breaks)` because one-character quoted note/mini-notation tokens matched.
Noise in the one field that has to be readable under pressure."""
p = _track(tmp_path, 'd7 $ n "0 1" # s "FMRhodes1" # legato "q"\n')
hint = oo.sounds_of(p)[7]
assert "FMRhodes1" in hint
assert "q" not in hint.split(", ")
def test_sound_hints_never_raise_on_an_orbit_with_no_quoted_name(tmp_path):
p = _track(tmp_path, "d5 $ silence\n")
assert oo.sounds_of(p)[5] == "?"
# --------------------------------------------------------------------------- #
# the setlist
# --------------------------------------------------------------------------- #
def test_the_real_setlist_loads_and_every_track_exists():
"""A setlist that silently drops a track silently drops a transition, so
load_setlist raises rather than skipping."""
setlist = oo.load_setlist()
assert len(setlist) == 13
for p, label in setlist:
assert p.exists()
assert label
def test_a_setlist_naming_a_missing_track_fails_loudly(tmp_path):
bad = tmp_path / "s.txt"
bad.write_text("live/does/not/exist.tidal # nope\n")
with pytest.raises(SystemExit):
oo.load_setlist(bad)
def test_setlist_comments_and_blank_lines_are_ignored(tmp_path, monkeypatch):
s = tmp_path / "s.txt"
real = oo.load_setlist()[0][0].relative_to(oo.REPO)
s.write_text(f"# heading\n\n{real} # label here\n\n# trailing\n")
got = oo.load_setlist(s)
assert len(got) == 1
assert got[0][1] == "label here"
# --------------------------------------------------------------------------- #
# integration against the REAL set — this is the test that would have caught a
# parser that only agrees with fixtures
# --------------------------------------------------------------------------- #
# Hand-measured by PLN + me on 2026-07-28, before this parser existed. If the
# parser and the hand count ever disagree, ONE of them is wrong and the tool must
# not be trusted until that is resolved.
HAND_MEASURED = {
"bombe_dj": {1, 2, 3, 4, 5, 7, 8, 9},
"wap": {1, 2, 3, 4, 5, 7, 8, 9},
"piment_bresilien": {1, 2, 3, 4, 5, 7, 8, 10},
"perfect": {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
"gimme_acid": {1, 2, 3, 4, 5, 8, 9, 10, 11, 12},
"vague_de_crime": {1, 2, 3, 4, 5, 6, 7, 8, 10},
"mafia_sans_serif": {1, 2, 3, 4, 5, 7, 8},
"you_my_sunshine": {1, 2, 3, 4, 5, 7, 8, 9, 11},
"desire": {1, 2, 3, 4, 5, 6, 7, 8, 9},
"the_revolution_will_be_sampled": {1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12},
}
@pytest.mark.parametrize("stem,want", sorted(HAND_MEASURED.items()))
def test_the_parser_agrees_with_the_hand_measurement(stem, want):
got = oo.orbits_of(oo.resolve(stem))
assert got == want, f"{stem}: parser {sorted(got)} vs hand {sorted(want)}"
def test_the_ear_report_reproduces_vague_de_crime_to_bombe_dj():
"""The ORIGINAL bug report, as a test. PLN heard the crimewave synth survive
into the next track; the mechanism says d6 and d10 are the survivors."""
assert oo.orphans(oo.resolve("vague_de_crime"), oo.resolve("bombe_dj")) == {6, 10}
def test_d6_of_vague_de_crime_really_is_the_crimewave_sound_he_heard():
assert "crimewave" in oo.sounds_of(oo.resolve("vague_de_crime"))[6]
def test_every_transition_in_the_real_set_is_analysable():
setlist = oo.load_setlist()
for (a, _), (b, _) in zip(setlist, setlist[1:]):
orph = oo.orphans(a, b)
assert isinstance(orph, set)
assert all(1 <= n <= oo.MAX_ORBIT for n in orph)
def test_orbits_1_to_5_never_orphan_anywhere_in_the_set():
"""Every track in the set declares d1-d5, so the core (drums/bass) can never
ghost. Worth pinning: if a future track drops one, the ghost lands on the
loudest possible orbit and this test says so before the stage does."""
setlist = oo.load_setlist()
for (a, la), (b, lb) in zip(setlist, setlist[1:]):
assert not (oo.orphans(a, b) & {1, 2, 3, 4, 5}), f"{la} -> {lb}"
def test_chain_and_matrix_render_without_raising(capsys):
setlist = oo.load_setlist()
assert oo.cmd_chain(setlist) == 0
assert oo.cmd_matrix(setlist) == 0
out = capsys.readouterr().out
assert "ORPHAN ORBITS" in out
assert "RESET POINTS" in out or "No track covers" in out
def test_chain_json_is_valid_and_complete():
import json as _json
setlist = oo.load_setlist()
oo.cmd_chain(setlist, as_json=True)
def test_pair_exits_nonzero_when_there_are_ghosts(capsys):
"""Exit code is the machine-readable answer, so a gig-up self-test (#44) can
gate on it."""
assert oo.cmd_pair("vague_de_crime", "bombe_dj") == 1
assert oo.cmd_pair("bombe_dj", "wap") == 0
def test_resolve_finds_a_track_by_bare_name():
assert oo.resolve("gimme_acid").name == "gimme_acid.tidal"
def test_resolve_fails_loudly_on_an_unknown_name():
with pytest.raises(SystemExit):
oo.resolve("definitely-not-a-track-xyz")
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