Commit cdbc28ff by PLN (Algolia)

feat(setbuilder): the picker now holds the catalog it already measured

The sweep evaluated all 703 tracks on Sunday night and found 597 that compile
and emit. The picker still carried fourteen hand-written entries, so the page
said the library was fourteen deep — the measurement existed and the surface
that needed it did not have it.

gen_setbuilder_catalog.py turns the sweep into the page's data, joined against
two things the verdict alone does not tell you: how often the driver journal
saw the track loaded (what the hands know), and how many of its sample banks
the preload plan already warms (a cold bank is not a fault, it just hiccups on
the first bar). Sample names come from setlist_samples.py, never a fresh regex.

The sweep results now live in docs/ instead of a scratchpad that dies with the
session — an artifact nobody can compare against is the preload.scd story.

Ids are repo-relative paths, not stems. Sixteen stems collide across folders
(major_nostalgia in chill and techno, all four FFF tracks in two trees) and a
stem key merged each pair into one card, silently. Picks stored under the old
stem keys are migrated on load where the stem is unambiguous.

And the Thu-24 list is a candidate arc, not a locked set. It was exported from
the builder, not decided at the desk; rehearsal decides.
parent 806c1e5d
......@@ -93,9 +93,11 @@ Open, highest leverage first:
saturated ±63 delta is unknown. Likely dead code. Re-run
`scratchpad/delta_probe.py` while PLN sweeps, read the delta histogram, THEN set the
threshold from reality. Builder also flagged 123 → 127 overshoot at the `>=115` band.
- [x] **THE SET IS LOCKED** — `armada/setlist_thu24.txt`, 10 tracks, 120/120 min, in play
order (commit 2fbb7d3). All 10 pass `silent-eval --seeded`, including
`something_about_drums` (never validated before). The set needs 46 banks, all 46 on disk.
- [ ] **CANDIDATE ARC, not locked** — `armada/setlist_thu24.txt`, 10 tracks, 120/120 min,
in play order (commit 2fbb7d3), exported from the Set Builder Tue 22. PLN's words Tue 22
evening: "this set is not locked, just a potential one." All 10 pass
`silent-eval --seeded`, including `something_about_drums` (never validated before); the
arc needs 46 banks, all 46 on disk. Rehearsal decides what stays.
- [x] **Preload covers the set** — `preload.scd` regenerated as a SUPERSET: 125 banks
(39-track 12-month list ∪ the 10 Thursday tracks), adding `bass2` for ete_a_mauerpark.
Warms at the next parvagues-sc boot. Backup in scratchpad; file is gitignored by design.
......@@ -118,6 +120,19 @@ Open, highest leverage first:
Bites on the next `systemctl --user restart perf-tray`.
- [ ] **techno_orage d2** (line 158) silent even seeded — fix or cut. PLN's call.
- [ ] **Preload gaps**: `bass2`, `humps` lazy-load mid-set. Regenerate after the arc is picked.
- [x] **The picker now holds the whole catalog** — the Set Builder showed 14 hand-written
candidates while the sweep had already measured all 703 tracks, which made the library
look 14 deep. `tools/gen_setbuilder_catalog.py` generates `armada/setbuilder-data.js`
from `docs/2026-09-22-catalog-playability.tsv` (the sweep, now durable in the repo — it
only lived in a scratchpad), the driver journal and `preload.scd`. Every row carries
verdict, area, play count, and how many of its sample banks are warm. 597 playable
tracks, filterable by text / area / played-here / fully-warm; 21 silent-orbit behind a
toggle; 85 compile-fails not listed. Per-area chip counts cross-check against the doc
(midi/nova 181, collab 98, techno 57, hip 41, boeuf 36). Ids are now repo-relative
PATHS, not stems: 16 stems collide across folders (`major_nostalgia` in chill and
techno, all four FFF tracks in two trees), and a stem key silently merged them into one
card. A one-line migration maps any previously-stored stem to its path, so a pick made
before this change survives.
### Play history — the real candidate pool (discovered Tue 22)
......
# THU 24 — 2026-09-24, the set in performance order.
# THU 24 — 2026-09-24, the candidate set in performance order (NOT locked).
#
# Hand-authored 2026-09-22 from PLN's Set Builder export (2h, 10 tracks,
# 120/120 min). NOT generated: there is no "## THU 24" section in backlog.md,
......
#!/usr/bin/env python3.12
"""Gen the Set Builder catalog: one row per track, with every signal that answers
"can I actually play this tonight?".
The picker used to carry fourteen hand-written entries. That was honest for the
night it was written — a shortlist from the driver journal — but it made the page
look like the catalog was fourteen tracks deep, when the sweep had already
measured all 703. This turns the measurement into the picker's data.
Signals, and what each one is worth:
verdict PLAYABLE / SILENT_ORBIT / COMPILE_FAIL, from tools/silent-eval.py
over every file under live/ (docs/2026-09-22-catalog-playability.tsv).
PLAYABLE means it compiles and every declared orbit emits events with
the #55 boot seed. It does NOT mean audible — see the doc's caveats.
banks how many sample folders the track names actually resolve to on disk.
An UNRESOLVED name is normal (a SynthDef, or structure overridden
downstream by `#`), so we count what resolves and never flag the rest.
warm how many of those banks the current preload plan already warms. A
cold bank is not a fault: it lazy-loads on first hit, which on stage
sounds like a hiccup on the first bar and is fine afterwards.
played how many times the driver journal saw this track loaded. Journal
retention is short, so this is "seen in the window", not all-time —
but it is the only record of what PLN's hands actually reach for.
Sample-name extraction is imported from tools/setlist_samples.py, never
re-implemented here: a `.tidal` file is a program, and `#` is `|>`, so a
hand-rolled regex is wrong in ways that fail silently.
Output is a same-origin data file the page loads, so the page stays hand-edited
UI and the data stays generated. Nothing here reaches the network.
python3.12 tools/gen_setbuilder_catalog.py
"""
import json
import re
import subprocess
import sys
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from setlist_samples import extract_names, build_index, bank_file_count # noqa: E402
REPO = Path(__file__).resolve().parent.parent
TSV = REPO / "docs/2026-09-22-catalog-playability.tsv"
PRELOAD = REPO / "preload.scd"
OUT = REPO / "armada/setbuilder-data.js"
# Two path segments deep for midi/nova, which is half the catalog on its own and
# would otherwise swamp every other area in the filter chips.
TWO_DEEP = {("midi", "nova")}
VERDICT_CODE = {"PLAYABLE": 0, "SILENT_ORBIT": 1, "COMPILE_FAIL": 2}
def area_of(rel):
"""`live/midi/nova/techno/x.tidal` -> ('midi/nova', 'techno')."""
parts = rel.split("/")[1:-1] # drop 'live' and the filename
if not parts:
return "root", ""
if len(parts) >= 2 and (parts[0], parts[1]) in TWO_DEEP:
return "/".join(parts[:2]), (parts[2] if len(parts) > 2 else "")
return parts[0], (parts[1] if len(parts) > 1 else "")
def preload_banks():
"""The bank names the current plan warms.
Same extraction as tools/check-preload.sh:121 so the two tools can never
disagree about what 'warm' means.
"""
if not PRELOAD.is_file():
print("! no preload.scd — every bank will read as cold", file=sys.stderr)
return set()
text = PRELOAD.read_text(errors="replace")
return set(re.findall(r"\[ \\([A-Za-z0-9_]+)", text))
def play_counts():
"""track -> times the lcxl3 driver logged loading it, over journal retention."""
try:
out = subprocess.run(
["journalctl", "--user", "-u", "lcxl3-driver", "-o", "cat"],
capture_output=True, text=True, timeout=60,
).stdout
except (OSError, subprocess.SubprocessError) as exc:
print(f"! journal unreadable ({exc}) — play counts omitted", file=sys.stderr)
return Counter()
names = re.findall(r"track -> (\S+)", out)
# Log lines read `track -> sept1:`; `test` is the scratchpad, not a track.
return Counter(n.rstrip(":") for n in names if n.rstrip(":") != "test")
def main():
if not TSV.is_file():
sys.exit(f"! missing {TSV.relative_to(REPO)} — run the playability sweep first")
index = build_index()
warm = preload_banks()
plays = play_counts()
rows, counted = [], Counter()
for line in TSV.read_text().splitlines():
if not line.strip():
continue
verdict, rel = line.split("\t", 1)
counted[verdict] += 1
src = REPO / rel
banks_warm = banks_total = 0
if src.is_file():
for name in extract_names(src.read_text(errors="replace")):
folder = index.get(name)
if folder is None or bank_file_count(folder) == 0:
continue # unresolved or empty: normal, not a fault
banks_total += 1
if name in warm:
banks_warm += 1
stem = Path(rel).stem
area, sub = area_of(rel)
rows.append({
"p": rel, # repo-relative path = the identity
"t": stem, # display title
"v": VERDICT_CODE[verdict],
"a": area,
"s": sub,
"b": banks_total,
"w": banks_warm,
"n": plays.get(stem, 0),
})
# Hands first, then warmth, then name — the order PLN would pick in.
rows.sort(key=lambda r: (-r["n"], -(r["w"] == r["b"] and r["b"] > 0), r["p"]))
payload = {
"generated": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
"source": str(TSV.relative_to(REPO)),
"counts": {k: counted[k] for k in ("PLAYABLE", "SILENT_ORBIT", "COMPILE_FAIL")},
"journal_window_tracks": len(plays),
"tracks": rows,
}
OUT.write_text(
"/* Generated by tools/gen_setbuilder_catalog.py — do not hand-edit. */\n"
"window.PV_CATALOG = " + json.dumps(payload, separators=(",", ":")) + ";\n"
)
print(f"wrote {OUT.relative_to(REPO)} — {len(rows)} tracks "
f"({counted['PLAYABLE']} playable, {counted['SILENT_ORBIT']} silent-orbit, "
f"{counted['COMPILE_FAIL']} compile-fail), "
f"{len(plays)} in the journal window")
if __name__ == "__main__":
main()
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