1. 07 Sep, 2026 3 commits
    • docs: the gig records are at Perso/www, not Web/www · 580e0000
      The documented path did not exist; resolving it cost a lookup while building
      gen_setlist.py. Also points at the judge_specs ear lists, which is where a gig
      lives before its tracks.json is built -- the cosmicfest set among them.
      PLN (Algolia) authored
    • fix(preload): warm what gets played, computed from the gig records · f68277f2
      The preload warmed a setlist FILE, and the only one was armada/setlist_opal2026
      .txt -- the 16 tracks of one August gig. So the plan was fresh, correct, and
      aimed at the wrong set. Measured: driving rose_rouge logged 41 'reading
      soundfile as needed' lines in 180s, every one of them the rose bank, while ZERO
      of the 60 warmed banks lazy-loaded.
      
      That is check-preload.sh's documented failure one level up. There the plan was
      stale because two tools disagreed about the setlist; here the plan matches its
      setlist exactly and the SETLIST has drifted from what gets played -- which no
      plan-vs-setlist freshness check can see, because the two agree.
      
      gen_setlist.py computes the list instead of maintaining one, from the canonical
      records in authority order: <www>/content/lives/<year>/<slug>/tracks.json, which
      carries both a date and each track's exact repo-relative file; then
      armada/tide-table/judge_specs/*_setlist_ear.json for gigs whose tracks.json is
      not built yet -- where the cosmicfest set lives, 'THE ground truth, 14 tracks',
      with rose_rouge at #5. Ear lists carry no gig date, so they are included and
      flagged rather than dated by guesswork.
      
      60 banks -> 111, 16 tracks -> 39. 51 banks were one first-play from a disk read.
      rose is covered.
      
      check-preload.sh now computes its own list rather than borrowing
      set-coherence.setlist_tracks(), because the two ask different questions: a cheat
      sheet wants tonight's running order, a preload wants anything that might get
      played, and the cost asymmetry is total. PV_PRELOAD_SETLIST / PV_PRELOAD_MONTHS
      override. Not a fourth setlist parser -- the same setlist_samples parser, fed a
      wider list.
      
      Cost of over-covering, measured: 122/124 banks OK in 5.0s (was 60/61 in 2.6s),
      scsynth RSS 4.8G on a box with 62G total and 40G available.
      
      Logs two findings from the same boot: 64 AppleDouble resource forks named
      zz._*.wav in rhadamanthe_fx/divers/vocal, which fail to load AND occupy sample
      indices (left alone -- the zz. prefix looks like a deliberate rename, so
      deleting is the owner's call); and preload's COUNT MISMATCH banner, which
      reports those 2 real load failures with the wrong diagnosis.
      PLN (Algolia) authored
    • fix(sc): every orbit's MiVerb/MiClouds/MiRipples send was a dead node · cd90dcb0
      On every clean boot the server said, 84 lines of it:
      
          *** ERROR: SynthDef global_mi_verb2 not found
          FAILURE IN SERVER /s_new SynthDef not found
      
      3 SynthDefs x 14 orbits x 2 lines. So '# verbwet', '# cloudswet' and
      '# ripplesreson' did nothing on any orbit, for the whole session -- silently,
      because a missing global effect is an absence of effect rather than a noise you
      notice.
      
      mi-UGens was installed correctly all along (11 .so + 11 classes) and the
      SynthDefs were defined correctly in start_and_midi.scd:273-296. The bug was
      ordering: SynthDef(...).add is ASYNCHRONOUS -- it compiles locally, sends
      /d_recv and returns -- while the very next statement sends /s_new for those
      names via initNodeTree. The /s_new overtook the /d_recv. One s.sync before the
      registration, legal inside s.waitForBoot's Routine; the idiom was already
      commented out twelve lines up. 84 errors before, 0 after.
      
      spectral-freeze above deliberately does NOT need it: it replaces a def whose
      synths are built per event, long after /d_recv has landed.
      
      Monitors it as gear, since presence checks could not see any of this:
      - mi-UGens extension now enumerates the 11 plugins AND 11 classes we use and
        names whichever is missing. d.is_dir() is true whether the directory holds a
        working install or a stale README.
      - SuperDirt boot errors (new) scopes the journal to the CURRENT boot and FAILs
        on SynthDef-not-found / FAILURE IN SERVER, naming the distinct defs rather
        than one line per orbit. Generic question, so it catches the next
        async-ordering bug too. WARNs when parvagues-sc is down, which is the correct
        on-demand state before a set.
      
      Both tested in both directions: FAIL on the recorded pre-fix journal window
      naming all three defs, PASS on the post-fix boot. That test is what caught 're'
      never having been imported in rig-doctor.py -- the new code was its first user,
      so the check would have crashed the doctor the first time it found something.
      
      Doctor: 47 checks, 0 fail, 10 warn, 37 pass.
      PLN (Algolia) authored
  2. 06 Sep, 2026 31 commits
    • perf(audio): the rig was reading samples off disk while playing · 077e1db1
      Went looking for latency to shave in the PipeWire quantum. The quantum was not
      the problem.
      
      preload.scd did not exist on this box. start_and_midi.scd:259 tests for it with
      File.exists and silently takes the other branch -- 'no preload.scd, lazy-loading
      samples on demand.' Driving rose_rouge headlessly for three minutes logged 46
      'reading soundfile as needed' lines, one every 5-12s, each a disk read landing on
      the audio thread; the SuperCollider node's pw-top W/Q hit 1.300 at the stock 1024
      and 1.390 at 512, i.e. missing its deadline regardless of buffer size.
      check-preload.sh named it exactly: '60 bank(s) would be read from DISK on first
      play (crackle, mid-transition)' -- the same sentence its own header attributes to
      a crackle that debuted at a venue.
      
      Generated with check-preload.sh --fix (60 banks, 16 tracks) and verified at the
      next boot: 'PRELOAD: 60/61 banks OK in 2.6 s'. The mechanism was never broken;
      gig-up.sh already regenerates the plan and the checker already exits non-zero.
      gig-up.sh had simply never been run on this box.
      
      Re-running the identical load after the fix left 29 lazy reads, ALL of them the
      rose bank -- because rose_rouge is not in setlist_opal2026.txt. Zero of the 60
      preloaded banks lazy-loaded, so the plan works; it warms the OPAL set while the
      track this box actually plays sits outside it. Logged, with two more findings
      from the same boot: preload's own COUNT MISMATCH banner firing on a
      sixty-second-old plan, and three global mi-UGens FX synthdefs failing to load at
      every boot while rig-doctor's mi-ugens check stays green.
      
      Adds tools/latency-lens.py, which implements the documented quantum method
      (lowest quantum holding zero xrun delta and max W/Q under 0.75) against pw-top,
      sets clock.force-quantum at runtime and restores it, and never edits a config.
      It refuses to run unless the default sink is muted and re-checks that every
      second for the whole run: a valid quantum test needs the real hardware device
      driving the graph but does not need the speakers.
      PLN (Algolia) authored
    • fix(watchdog): a boot is not a death, and the rig only gets three starts · b9564095
      parvagues-sc.service is Type=simple, so systemd calls it active the instant
      sclang execs -- but scsynth is sclang's child and appears ~8s later. The loop
      acted at MISSES_TO_ACT*POLL_SECS = 6s, so every healthy start earned a restart:
      
        23:17:12 scsynth GONE (3 polls) while parvagues-sc.service is active
                 -- restarting (0 prior in window)
        23:17:21 scsynth up after 8s
      
      '0 prior in window' on a start where nothing was wrong. The restart was not the
      damage; the rate limit was. Each clean start spent two of systemd's
      StartLimitBurst=3, so a second start inside StartLimitIntervalUSec=5min hit the
      limit and left the unit failed/start-limit-hit -- refusing to start at all until
      reset-failed. On stage that is indistinguishable from a dead rig.
      
      BOOT_GRACE_SECS=30, measured from the unit's own ActiveEnterTimestamp, and it
      says 'holding off' in the journal rather than waiting silently. Misses keep
      counting through the grace so an expiring grace acts at once. The per-poll
      is-active became one show returning both fields: 7.0ms -> 9.4ms, +0.12% of a
      core, measured not assumed.
      
      Also cures the watchdog of the same pgrep -x that cost protect a tenth of a
      core -- 52.9ms per poll while SC is up (3.0% of a core WHILE PLAYING) against
      20.8ms for one stateless /proc/*/comm pass, zero forks. Duplicated from
      parvagues-protect rather than shared: that script is copied to /usr/local/bin
      and runs as root, so it must not source anything from a user-writable repo.
      
      rig-doctor's check_protect now asks whether protection WORKS: the running
      daemon's live CapEff from /proc must include cap_dac_override, and the daemon's
      own --check must pass. It was green all through the day the guard could not
      write a single oom_score_adj, because it tested that two files existed.
      
      Verified end to end on the rig: protect caught pids created 9min after it
      started (oom:200->-1000 on sclang[257669]), and the watchdog held off at 6s and
      spent none of the rate limit. Suite 11/11 incl. a new slow-boot regression case;
      rig-doctor 46 checks, 0 fail.
      PLN (Algolia) authored
    • perf(protect): the guard was spending a tenth of a core to discover nothing had changed · a828d5f0
      219.1ms per 2s tick, 6 forks, with NOTHING running to protect. None of it was
      protection: three pgrep -x at 59ms each (pgrep reads cmdline for every process
      on the box, and it ran once per target), plus cat, two chrt -p reads and an
      unconditional prlimit per pid, each in its own command substitution.
      
      Now zero forks in the steady state: one bash pass over /proc/*/comm, sched from
      /proc/<pid>/stat fields 40/41, prlimit only when /proc/<pid>/limits says so.
      28.5ms/tick, 1.42% of a core at the unchanged 2s interval.
      
      The interval is deliberately untouched -- the 7.7x came from forks alone, so the
      responsiveness that re-protects scsynth before its first sound was not traded
      for battery.
      
      Verified differentially against the old pgrep path with a decoy fleet (ardour9,
      ArdourGUI, ardour-8.6 must match; ardour-decoy, sclang-notreally, scsynthx and a
      bash whose path contains 'ardour' must not) and against chrt -p / cat on live
      scsynth+sclang. Needs sudo tools/install-protect.sh to take effect.
      
      Also logs two findings from the same measurement pass: sc-watchdog restarts
      SuperDirt on every clean start (6s patience vs 8s boot) and two starts in five
      minutes hit StartLimitBurst, leaving the rig unstartable; and the other two
      reconcile loops cost 5.3% between them with real event sources available.
      PLN (Algolia) authored
    • Pre-compact cleanup: CHANGELOG + archive + memory · 9150a200
      Sprint 3 (the box plays alone, and the guard now guards), the archive entry with
      its five learnings, and three deferred items: rig-doctor should call
      parvagues-protect --check instead of testing for files, an install step should
      fetch the sister repos, and the protect daemon's 10.7%-of-a-core poll needs
      fewer forks.
      PLN (Algolia) authored
    • fix(protect): the installer was not idempotent, so the fix did not take · 4f125e14
      Reinstalled after adding CAP_DAC_OVERRIDE and nothing changed: the unit on disk
      was correct, daemon-reload had run, NeedDaemonReload said no — and the running
      process still held the old three capabilities, same PID and start time as before
      the install.
      
      `systemctl enable --now` starts a STOPPED unit and does nothing to a RUNNING
      one. So every re-install silently kept the previous daemon, with the previous
      unit's capabilities and the previous script's inode, while printing success.
      
      - restart instead of enable --now (it holds no ports and makes no sound)
      - assert the LIVE /proc capability set after restart, and say plainly that
        protection is decoration until that line reads ok
      
      Third layer of one lesson tonight: a correct file on disk is not a correct
      process in memory.
      PLN (Algolia) authored
    • backlog: tidal-ears cloned, GLITCHWAVE synced, HUD pushed, protect installed · 22a045be
      Four items closed on the portable-rig front, and one new one opened: no install
      step fetches the sister repos CLAUDE.md documents, which is why three separate
      manual fixes were needed tonight for one missing line of setup.
      PLN (Algolia) authored
    • fix(protect): root was never enough — the OOM guard could not open the file it guards · 8f2c510f
      Installed the protection daemon and it reported 'protected: ardour[...]
      oom:FAILED(need root, have uid 0)' every two seconds. It runs as root; the
      message was misdirecting.
      
      uid 0 bypasses file permissions via CAP_DAC_OVERRIDE, and the unit's
      CapabilityBoundingSet listed only CAP_SYS_RESOURCE/SYS_NICE/IPC_LOCK — so the
      bypass was gone. /proc/<pid>/oom_score_adj is mode 0644 owned by the process
      owner, so every write returned EACCES while id -u still said 0.
      
      scsynth and sclang looked fine only because they were already at the target and
      no write was attempted. The daemon had never successfully protected anything.
      
      - unit: add CAP_DAC_OVERRIDE to bounding + ambient sets
      - failure messages print the effective capabilities, not 'need root'
      - a FAILED sweep logs once per distinct message instead of 43200 times a day,
        which is what the file header already said it refuses to do
      PLN (Algolia) authored
    • Pre-compact cleanup: CHANGELOG + archive + memory · b82215ff
      Sprint 2 (the board says what is playing), archive entry with the night's
      learnings, and the backlog corrected where the fader feed was listed as unbuilt.
      Records the one leg still unverified: Ardour's own echo has not been observed
      arriving, only proven linked.
      PLN (Algolia) authored
    • feat(lcxl3): the d9-12 level rings learn what Ardour holds · 3a91eba0
      The driver's own startup line said the gap out loud: "row A stays absolute:
      A1-A4 are Ardour's, we hold no truth for them". So those four rings sat at a
      flat rest colour while every other cell painted its value.
      
      Two sources close it, and they are complementary rather than redundant:
      
      1. A forwarded CC is an observation. When PLN moves A1-A4 the driver TRANSLATES
         that value on its way to Ardour, so at that instant it knows what the fader
         holds. It always recorded it in self.values; only ring_colour's
         Ardour-owned branch threw it away. Now it also marks the cc observed, and
         the ring breathes by audibility from the first touch.
      
      2. Ardour echoes what it did NOT get from us. `<Protocol name="Generic MIDI"
         feedback="1" motorized="1" active="1">` was already on, and Ardour will not
         echo a change back to the surface that caused it — so source 1 covers the
         hand on the knob and source 2 covers the GUI, the mouse, automation and a
         session load, which is exactly where source 1 goes stale.
      
      The feedback leg needed its own port. The driver already sends into Midi
      Through (14:0) and Ardour's Generic MIDI reads from there, so listening on that
      loopback would have fed the driver its own CCs back: no loop is possible now
      because the send path and the echo path share nothing. And it is the one
      binding here that lives in JACK rather than ALSA, because Ardour's control port
      is a JACK port and only PipeWire's bridge makes ours visible there — so the
      bridged name, which carries a playback index nobody should guess, is DISCOVERED
      by suffix on every pass and re-asserted like every other binding on this rig.
      
      Verified on the live rig, Ardour closed then reopened: the port publishes
      (client 132 'ParVagues LCXL3 FB'), the bridge exposes it, the reconciler finds
      and links it unaided, and it re-links within 5 s of a driver restart. Absent
      Ardour it stays silent and returns False, costing nothing but the rest colour.
      PLN (Algolia) authored
    • docs(backlog): visuals done — and the correction needed correcting · 07bb4230
      'The refs work as designed' held only on the box that authored the image.
      resolve() never tried the derived twin when the absolute source was absent, so
      every ref resolved to null on the XPS24 with the mp4 already synced. 0 of 5
      targets resolved before the HUD fix, 5 of 5 after.
      PLN (Algolia) authored
    • docs: correct the visuals plan, and record the jig decision · 327d410c
      The visuals entry was wrong in its premise, and the correction matters more
      than the task: xps22 came up and showed that `visuals/scenes/` already exists
      (14 mp4s, 197 M), that the HUD's scenesDir already points at it, that the gifs
      were already compressed on 2026-08-29, and that committing them was
      DELIBERATELY rejected in .gitignore with the reason written in. The "16 dangling
      url refs" are working as designed — scenes.js:_preferDerived() documents that
      the header points at the SSOT source and the HUD substitutes the derived mp4.
      
      So the plan "one home, compress, rewrite refs repo-relative" proposed doing
      three things that were already done or already decided against. The remaining
      work is a 197 M copy, and the entry now says so. Kicked a fence before reading
      why it was there.
      
      Jig decision 1 answered: ONE STABLE JIG, swept every time. Recorded with the
      two consequences that follow — ARDOUR_SESSION stays fixed, and "self-contained
      per gig" must now come from take metadata, which folds question 2 into it.
      
      Also opened: the Ardour fader feed's verified findings (Ardour has already
      learned CCs 13-16; only "midi-feedback"=0 withholds the values; Midi Through is
      a loopback so the feedback leg needs its own port), ../tidal-ears not being
      cloned here, and pytest being absent from the very interpreter the rig runs.
      PLN (Algolia) authored
    • feat(gig-up): put the mix back on every launch — the faders are not state · 6d9c9ef6
      PLN, asked whether a `Tidal 12` parked at -inf was a mistake or intent:
      "tidal 12 -inf is random i might turn on/off any fader its not signal
      throughout perf it mioves always and i save random". So the question was wrong.
      Whatever gain Ardour saved is wherever a hand left it mid-set; a session's
      at-rest mix is noise, and there is nothing to diagnose.
      
      Which is why the readiness gate could only ever REPORT it. Measured just now,
      the drift is not one fader but NINE of twelve, five of them at -inf: 02 03 04
      05 08 12 fully silent, 06 07 10 down 40+ dB. A mix that scatters every set
      needs a mechanism, not a warning.
      
      So restore it, in the window the source sweep already owns — Ardour closed.
      fader-baseline REFUSES while Ardour runs (writing under a live session desyncs
      it from the desk, and the desk wins on next touch), which makes "post-close"
      and "pre-launch" the same safe moment. Full restore to the 2026-08-02 baseline
      per PLN's call, not just a rescue of dead faders: it keeps a .prefader.bak, is
      idempotent, and holds a 0.5 dB tolerance so it never churns.
      
      Verified both branches against the live rig: --check reports the 9-fader drift,
      and --restore correctly refuses with Ardour open.
      PLN (Algolia) authored
    • feat(lcxl3): the rings breathe at the tempo when a control is engaged · 0824a277
      Motion on the board now means one thing: THIS IS MAKING SOUND. It used to mean
      only "a DJF sits at its zero mark", because animated() hard-returned False for
      every role but family_filter.
      
      Three laws, all at bar rate (the slow breath, not the beat one):
      
        fx / fx2, every orbit   breathe whenever the knob is off zero
        level, d9-d12 only      breathe whenever the level is not off. Only those
                                four, because d1-d8 levels live on row D and FADERS
                                HAVE NO LEDS — painting them was already called a
                                no-op lie in paint_cell, and asking for it here would
                                have been the same lie one layer up
        boolean cells           dark until 127, breathing at 127
      
      That last law needs to know what a control IS, so parse_kinds() reads it off
      the track: a cc reached through `# effect (... "^NN" ...)` is a dial, a cc
      reached through midiOn/midiOff is a switch. Tidal applies midiOn's function
      while the CC reads high, so such a control is boolean however many steps the
      hardware sends — and a ring fading up across a range the music ignores is a
      ring that lies about the sound. When one cc is used both ways, continuous wins:
      the value demonstrably matters somewhere, so fading is the honest paint.
      
      The breath's CEILING is the value (_breathe_to): the brightest instant equals
      _lightness(value) and never exceeds it, so "lightness carries VALUE" still
      holds while motion carries engagement. A knob at 30% breathes dim.
      
      Levels stay honest. Ardour owns cc 13-16 and has not told us their values, so
      ring_colour(value_known=False) keeps the flat rest colour rather than breathing
      on a guess — the oldest rule here is that the LEDs do not lie. The feedback
      feed that fills ardour_seen comes next; until it does, those four rings look
      exactly as they did.
      
      Also folds two defaults that had drifted: paint used 0 for an unseen control
      and the glow tick used 64. value_of() settles it — an unseen DJF is at its
      centre detent, because that is where the knob physically rests; anything else
      unseen is OFF, because engagement we have never observed must not be claimed.
      
      Checked against rose_rouge: B8+C8 (d8's stacks, the ones asked about) classify
      bool; A6/A7/A8's delay and squiz knobs classify cont; breath peak equals the
      static value at every level; DJF behaviour unchanged; unmapped still black.
      PLN (Algolia) authored
    • fix(tests): one absent sister repo and the ratchet guarded nothing · 761d8589
      tools/analyze_samples.py is a COMMITTED symlink into ../tidal-ears, which is
      not cloned on the XPS24. rglob could see the path and read_text could not open
      it, and _offenders() caught only SyntaxError — so a FileNotFoundError escaped
      at import time and the whole module failed COLLECTION.
      
      That is the exact failure this file exists to prevent, turned on itself: the
      guard reported nothing rather than reporting a gap, and a guard that cannot run
      guards nothing. Skip unreadable paths explicitly.
      
      Verified standalone on the rig interpreter: 3 tests pass, 256 files scanned.
      PLN (Algolia) authored
    • fix(gig-up): launch Ardour through pw-jack — one audio server, not two · 7a54a1f8
      The night rose_rouge first played on the new laptop, this cost an hour and
      presented as three unrelated faults.
      
      Both jackd2 and pipewire-jack are installed, and ldconfig resolves libjack.so.0
      to JACKD2's — nothing in ld.so.conf.d prefers PipeWire's. So a plain `ardour` on
      the JACK backend finds no jackd server, STARTS one, and jackd claims a card
      through the D-Bus device reservation. Observed: jackd came up on `hw:NVidia,3`
      and reserved Audio0, so Ardour's audio left by an HDMI port with nothing plugged
      into it, while scsynth sat in PipeWire's graph on the speakers. Two graphs that
      could not see each other — qjackctl listed Ardour and no SuperCollider, and
      nothing was audible from anywhere.
      
      And the ALSA backend is not the way out, it is the worse failure: it reserves
      BOTH cards, PipeWire releases them, and the laptop drops to a single "Dummy
      Output". No Tidal, and no Zoom either, on the machine that has to do both. That
      is the state this box was found in, and the reason all three symptoms had one
      cause.
      
      pw-jack only sets LD_LIBRARY_PATH to PipeWire's jack dir — precisely how the SC
      unit already arrives there (scsynth maps pipewire-0.3/jack/libjack.so.0). Same
      mechanism, same reason: ONE server, so the orbits and Ardour share a graph and
      the desktop keeps its cards. launch_bin gains an optional LAUNCH_WRAPPER, empty
      for every other app.
      
      Verified end to end: Tidal -> SuperDirt :57120 -> scsynth (28 outs, PipeWire
      JACK) -> ardour:Tidal 01..12 -> Master -> Speaker playback_FL/FR, with no stray
      jackd, both cards held by WirePlumber, no double path on d1, and
      check-audio-graph.sh green but for "no UMC on the bus" (it is not a venue).
      PLN (Algolia) authored
    • feat(gig-up): clear the session's dead references before Ardour opens · 8728293d
      The Missing File modal is one dialog per dead source, cannot be suppressed by a
      flag, and sits between a click and a playable rig (#136). On 2026-09-06 it cost a
      launch outright — and invisibly, because launch_bin throws app output at
      /dev/null: Ardour sat for 13 minutes with 0.08s of CPU and no window at all,
      while the only thing that could have said why went to the bit bucket.
      
      So gig-up sweeps the session before opening it, never while it is open (Ardour
      saves its in-memory session over the edit on quit and would undo it). Idempotent:
      a clean session prints "nothing to do" and writes nothing.
      
      The mount check IS the safety story. --allow-archived overrides the tool's "this
      survives elsewhere, restore it instead of dropping the reference" refusal — the
      right answer for an accident, the wrong one for deliberate archival, and the only
      thing telling those apart is whether the archive is actually there. With the
      mirror unmounted the tool cannot distinguish a filed take from a lost one, so it
      is never handed the flag. A reference is the last breadcrumb pointing at a take;
      dropping one blind turns a restore into a forensics job. Neither branch ever
      blocks the gig.
      
      Also writes down what tonight cost, in the cheatsheet where it will be reread at
      setup rather than in a log nobody opens mid-gig: Ardour's per-port Incoming MIDI
      gates the faders no matter how right everything else is; which of the three LCXL
      ports is the correct one and why the other two are wrong; that no-sound-no-
      highlight is a stale GHCi (tidalcycles:reboot) and not the rig; and that
      boot-superdirt would start a second SuperDirt to fight parvagues-sc for :57120.
      PLN (Algolia) authored
    • fix(lcxl3): survive an unplug/replug — the board comes back lit · 8229893d
      PLN, setting up: "ill often unplug replug another port a controler when setting
      up, it needs to resist that, atm i unplugged replugged lost the led" — and then,
      with the surface dark, "movign faders dont move sound for now".
      
      A replug destroys the kernel's sequencer client and rebuilds it, dropping every
      subscription on it. rtmidi is never told, so the driver kept its handles, kept
      printing "translating", and kept painting into a void: 129:0 had no "Connecting
      To" and the input leg no "Connected From", while the board sat there dark and
      deaf. reconcile() reported healthy all the way through, because the leg it
      watches — the virtual port into SuperCollider — is virtual-to-virtual and
      survives the event untouched. The half that breaks was the half nobody checked.
      
      Names cannot detect this and that is the trap: the board came back as client 24,
      the SAME number, so find_ports() answered, in_name still matched, and every
      string looked right. Only the subscription knows.
      
      So the reconcile tick now checks the board link first and relinks on loss:
      reopen the Surface, re-assert DAW mode and the relative rows, repaint, relabel.
      Board before SC, because a dark surface is the louder failure.
      
      Two things learned by testing it rather than trusting it:
      
      - ANY-subscription is not a health signal. The first cut asked "does the DAW port
        have a subscriber?" and answered True on the exact state it exists to catch —
        a replug leaves DEBRIS, and aconnect went on printing the board wired to
        clients 132/133 that no longer existed while our own ports sat detached. It now
        matches aconnect's pid= against os.getpid() and requires OUR client on both
        ends: no input is a dead controller, no output is a dark one.
      - A relink does not re-seed. Seeding pushes values downstream, and downstream is
        the one path that never broke — SC already holds them.
      
      Verified by cutting both legs with `aconnect -d` (what the replug does to the
      subscriptions) and watching it come back within one tick.
      PLN (Algolia) authored
    • docs(ardour): the session is a jig, not an archive — plan + a stale command caught · 796afa6b
      Tonight's launch died on the Missing File modal, so the references got counted
      properly: 294 missing sources, 270 of them alive on the Freebox mirror (53G, no
      file under 1MB), and 24 that exist nowhere — all of Take101, 12 orbits x L/R,
      already written off in 041 as "tests at best".
      
      The plan promotes the drop-missing-sources doctrine from a one-off repair to a
      standing invariant: the session holds tracks, routing and levels, and zero audio
      sources at rest. Structure comes from a template (correct by construction, rather
      than mutating a session back to empty and having to be right about what to
      remove); state is kept by an idempotent sweep that runs at LAUNCH as well as
      close, because a close-only hook is bound to the least reliable moment there is
      and a crashed gig would either strand stems or eat them.
      
      Also names the tension it has to live with: the tool refuses to drop a reference
      whose audio survives elsewhere, which is exactly the state deliberate archival
      creates. --allow-archived is the answer, and the rule is that only a caller which
      has itself verified the copy may pass it. A human running it blind stays refused.
      
      And a correction 043 could not have known it needed: the one-liner recorded there
      now refuses, because the eleven other takes moved to the mirror after it was
      written. The doc was right when written and wrong now — drift a plan should catch
      instead of a gig.
      PLN (Algolia) authored
    • fix(doctor): it was asking the wrong interpreter, about the wrong binding · 13764f73
      Two faults that hid each other, both found by reading the journal — which is
      exactly the work this doctor exists to save.
      
      It probed sys.executable. PLN's PATH starts with a pyenv shim, so the natural
      `python3 tools/rig-doctor.py` answered for pyenv 3.11.10 — an interpreter no unit
      ever uses; all five python units run /usr/bin/python3. That made the doctor wrong
      in both directions at once: it called mido and PyQt5 MISSING while every unit
      using them ran fine, and it still could not see the dep that was genuinely gone.
      A check that cries wolf is a check nobody reads the day it is right. So it now
      asks the interpreter the rig actually runs on.
      
      And the dep it could not see: midiviz imports PySide6, and has since it was born
      (30a704ec), but the table listed "perf-tray, midiviz GUIs" against PyQt5 alone.
      So the doctor asked about a binding midiviz does not import, found it, and
      reported green while midiviz crash-looped at RestartSec=5 on ModuleNotFoundError
      for a whole session, PLN's lens simply absent. Two GUIs, two bindings, two rows.
      
      Run the natural way it now says: CAN THIS BOX PLAY A SET? no — python: PySide6.
      PLN (Algolia) authored
    • fix(tray): the only UI on the rig ignored a left click · 5e030c78
      GNOME's AppIndicator extension turns button-1 on the tray icon into a
      StatusNotifierItem Activate() call, Qt re-emits it as activated(Trigger), and
      nothing was connected to it — so the most obvious gesture on the one UI that is
      meant to be reachable mid-set did nothing at all. Only button-3 opened anything,
      and that menu is drawn by the shell from the exported DBusMenu, not by us.
      
      Qt5 hardcodes ItemIsMenu=false, so the host will never open the menu for us on a
      left click; popping it at the cursor is the only route. Middle click too — a
      hidden third behaviour mid-set is worse than a redundant one.
      
      popup(), not exec_(): exec_() spins a nested modal event loop, which would stall
      the 2s refresh timer and the gearbox reads behind it for as long as the menu
      stayed open.
      PLN (Algolia) authored
    • docs(archive): #29 and #17 — the portable rig proved on a second box · 519ad30a
      Long-form entries for the two tasks the XPS24 session closed. Written for a
      reader months from now with no memory of it, because these are the source
      material for the blog post (#22) that was deliberately blocked on this outcome
      so its ending could be 'it played' rather than 'it should'.
      
      The through-line worth keeping: six blockers on the second machine and not one
      of them produced an error message. A hijacking startup.scd, a quark cloned but
      unregistered, tidal built against the wrong ghc, a 644 installer, no pip at all,
      and a Pulsar package whose documented install command silently downgrades the
      rig. Every one of them was invisible on the box the setup was written on, which
      is the actual lesson: config that has only ever run on one machine has been
      observed working, not tested.
      
      Also recorded, because they are about the measuring instruments rather than the
      rig: the doctor's coverage check counts bank names and so reported all 281 banks
      present while 15 GB was still copying, and its fix text for missing units named a
      tool that cannot install them. And the tilde traps -- File.exists('~/x') is
      false, nowExecutingPath is nil inside waitForBoot -- each of which would have
      turned a portability fix into a permanently-false guard.
      PLN (Algolia) authored
    • fix(install): the installer shipped non-executable, and the doctor sent you to the wrong tool · 51f1189e
      Both found the only way they could be found: by running the documented
      procedure on a machine that was not the one it was written on.
      
      **tools/rig-install.sh was committed as mode 100644.** It is the first command
      in SETUP.md's fast path. It worked for its whole life on the authoring laptop
      because the author had chmod'd it there and never committed that — a local
      chmod fixes your tree, not the repo, and the index is the only mode that
      travels. So `tools/rig-install.sh` on a fresh clone died with "permission
      denied", on the one machine the script exists to serve. Five other tracked
      shell scripts had the same mode (gpu-mode.sh, init_midi.sh, viz/launch.sh and
      two under armada/); all six are now +x in the index.
      
      Fixing the six without a guard would just wait for the seventh, so
      tools/tests/test_scripts_are_executable.py reads modes out of `git ls-files -s`
      — the index, not the working tree — and fails on any tracked *.sh at 100644.
      It also pins the five named entrypoints explicitly so a rename cannot quietly
      drop one, and it guards itself: an empty file listing must not read as
      success. Mutation-verified in both directions (set gpu-mode.sh back to 644 →
      1 failed naming the file and printing the git update-index fix; restored →
      3 passed).
      
      **The doctor's fix text for missing units pointed at rig_units.py.** On a
      fresh box that is wrong in a way that wastes real time: rig_units.py only
      enables and starts units that are ALREADY symlinked into
      ~/.config/systemd/user/, so `--ensure --apply` returns fifteen consecutive
      "Unit file does not exist" lines and reads like a broken reconciler. It isn't
      — rig-install.sh is what creates the symlinks. The check now looks at whether
      the units are merely not-enabled or genuinely not-found, and points at the
      installer in the second case.
      
      That distinction was already in the data (rig_units.py --status prints
      "not-found" in the enabled column, and the doctor already parsed that column
      into rows) — nobody had read the two facts together, because on a box where
      the symlinks have existed for months the not-found branch never fires.
      
      901 passed before, 309 in tools/tests after adding the new file.
      PLN (Algolia) authored
    • fix(portable): the rig stops assuming it lives at /home/pln — and three tilde traps on the way · b130508c
      SRE #84 phases 1 and 2. Five files, ~30 load-bearing lines, and the point is
      not tidiness: on a box where the account is not `pln`, SuperDirt booted
      completely cleanly with every one of the three sample roots silently absent,
      and the LCXL painter units exec-failed while rig-doctor reported the LCXL
      section green. Two silent failures, each standing behind something that looked
      like a pass.
      
      **Phase 1 — the three sample roots and the two same-repo loads.**
      start_and_midi.scd and sample_watch.scd held /home/pln as literal strings.
      The fix is not a blind sed, because a raw tilde expands nowhere by itself.
      Three traps, all verified on sclang 3.14.1 before writing a line:
      
        - `"~/x".standardizePath` keeps a trailing glob intact, so the loadSoundFiles
          calls can stay one-liners. Good.
        - `File.exists("~/x")` returns FALSE. A naive tilde substitution would have
          left the preload and sample-watcher guards syntactically perfect and
          permanently false — a dead preload with no error, which is worse than the
          hardcoded path it replaced.
        - `thisProcess.nowExecutingPath` is nil inside s.waitForBoot's closure. The
          two same-repo loads live in there, so the repo root is captured at TOP
          level into ~pvRepoRoot and used later. Reading it in place would have
          produced exactly the same dead-guard failure as the tilde.
      
      Deriving the repo root from the file's own location beats a home-relative
      guess: the rig now runs from any clone path, not merely any username.
      
      **Phase 2 — the two outlier units.** lcxl3-driver.service and
      lcxl-leds-watch.service baked WorkingDirectory and an absolute ExecStart,
      where every sibling unit (midiviz, perf-tray, parvagues-bridge,
      midi-autoconnect, tidal-ardour-autoroute) already used %h. Now they do too,
      verified with systemd-analyze --user verify.
      
      **Phase 3, unplanned — the fix broke the check that guards the fix.**
      rig-doctor's check_scd_sample_roots reads the roots OUT of start_and_midi.scd
      rather than hardcoding them, which is the right design and is why it caught
      this at all. But it then called Path.is_dir() on the extracted literal, and
      Python does not expand a tilde either — the same trap as File.exists, one
      language over. So the portability fix turned three PASSes into three false
      FAILs on the box where all three roots exist. That is how a check gets
      switched off. It now expands before testing, and reports the resolved path so
      the reader can see what was actually stat'ed.
      
      Validation: both .scd files compile (File.readAllString(f).compile, which
      parses without executing); both units pass systemd-analyze verify; rig-doctor
      goes from 3 fail / 36 pass to 0 fail / 39 pass with the same verdict logic;
      901 passed, 2 skipped across the suite.
      
      The lesson for the log is the third phase, not the first two. A checker that
      derives its expectations from the source it checks is strictly better than one
      with hardcoded copies — and it still shares the source's bugs, one runtime
      removed.
      PLN (Algolia) authored
    • docs(archive): the portable-rig trio — sample-pack, rig-doctor, clone-the-gig · 1d3bb807
      Three tasks that turned 'can PLN play from another laptop' from a question into
      a procedure, archived long-form because the learnings are the blog material.
      
      The through-line worth reading later: every one of the three had a structural
      flaw that only appeared when you asked where the tool would RUN. sample-pack was
      designed as two bundles until PLN reframed it as one parameterized query over the
      mapped list. rig-doctor was green at home and structurally incapable of naming
      absence on a fresh box, because it resolved against the machine it ran on.
      rig-install would have created an empty quark directory that the next run reads
      as 'already installed'. None of the three was caught by a test; all three were
      caught by asking what happens on a machine that has nothing.
      PLN (Algolia) authored
    • feat(tools): rig-install.sh + SETUP.md — clone the gig onto a new box · 2d51973f
      The install counterpart to rig-doctor.py: the doctor only ever reads,
      this converges. Idempotent by construction — detect, report, act only
      if something is actually missing, never fail the whole run for one
      optional piece. Covers every checked item in rig-doctor.py: system
      packages (printed, pacman+--yes is the only path this script ever
      executes itself), SuperCollider quarks at current HEAD (no lockfile —
      PLN rejected pinning), the mi-UGens extension, the private
      parvagues-synths repo + its link.sh, the two ~/.local/bin shims two
      systemd units silently crash-loop without (nothing in the repo created
      these before now — a top-5 fresh-box breakage), the systemd --user
      units from rig_units.py's one authored table, and the sample carry-on
      bundle (rsync, out of band from git on purpose).
      
      Root-owned installs (perf-audio+sudoers, parvagues-protect, the
      [midiviz-pin] KWin rule) are print-only, always — this script never
      runs sudo. A script that quietly escalates itself is one nobody can
      audit before a gig; `--print-root-steps` shows just those three
      commands and does nothing else.
      
      Every path is resolved at run time from the script's own location,
      $HOME, and $XDG_DATA_HOME — never baked in. This rig's oldest
      recurring bug is a binding resolved once and never re-resolved; an
      installer is exactly the kind of code tempted to commit that bug by
      writing down what it found on its own author's box. For the same
      reason it never repoints an existing systemd-unit or ~/.local/bin
      symlink that already resolves correctly, even to a DIFFERENT checkout
      than this one — only creates what's entirely missing, or repairs a
      dangling link. Tested twice on PLN's own performing laptop (already
      fully provisioned): both runs report "already ok" throughout and
      change no running state, except parvagues-synths/link.sh adopting 18
      already-identical synthdefs as symlinks (its own designed
      no-op-equivalent).
      
      Also regenerates tools/parvagues-rig.target from rig_units.py's
      generator: it had drifted (still Wanted the now-manual lcxl3-driver,
      missing midiviz) since the unit table changed under it.
      
      SETUP.md is the human procedure: the fast path, what rig-install.sh
      does and doesn't do, the LCXL2/LCXL3 hardware-detected split, a
      troubleshooting section keyed off the doctor's real FAIL text, and an
      explicit "known not portable yet" — start_and_midi.scd and
      sample_watch.scd hardcode /home/pln, so a non-pln account gets a rig
      that boots clean with banks and preload silently missing (task #28).
      Named honestly rather than hidden from the next person who hits it.
      PLN (Algolia) authored
    • feat(rig): the doctor can name what a fresh box is missing, before the audio arrives · 06039c8a
      rig-doctor's samples check had a structural hole I only saw once it ran: it
      shells out to sample-pack, which resolves bank names by walking the Dirt-Samples
      tree of the box it is running on. On PLN's laptop that is 281 banks and looks
      fine. On an empty XPS24 it reports "0 banks resolve" — true, and useless. The
      check could never say "281 missing", which is the only thing a fresh machine
      needs to hear.
      
      The fix is an EXTERNAL reference, committed: sample-manifest.json at the repo
      root (463 KB, 281 banks, 9385 filenames, generated by tools/sample-pack.py
      --all). rig-doctor now falls back to it when no --bundle is given, and --bundle
      accepts a manifest FILE as well as a bundle directory. So the ordering that
      makes a same-day setup possible works: git clone, run the doctor, get the exact
      list of missing banks — all before 15 GB of audio has moved anywhere.
      
      Committed deliberately, not gitignored. A generated artifact that nothing ever
      compares against reality is how preload.scd drifted for weeks and made its
      debut as a crackle at a venue. This one is diffable, and its own freshness is
      visible in git.
      
      Mutation-verified rather than assumed, because a checker that cannot report
      absence is exactly the failure being fixed here: planted two nonexistent banks
      in a copy of the manifest, confirmed the check goes FAIL, names them, counts
      2/281 with sizes, and flips the final verdict from "yes" to "no".
      
      That test also exposed a fix-pointer that could not be followed — with a
      manifest file it said "see <manifest.json>/README.md", a path that cannot
      exist. Now _bundle_fix() distinguishes the three cases: you have the bundle
      directory (cp from it), you have a bundle's MANIFEST.json (cp from its parent),
      or you have only the committed list (rsync from the freebox staging copy, or
      rebuild with sample-pack on a box that has the samples). A fix you cannot follow
      is not a fix.
      
      The carry-on bundle it describes is built and verified: 281 banks, 9385 files,
      15,108,195,468 bytes, checked file-by-file against its own manifest with 0
      mismatches, and staged to /mnt/freebox/PLN/parvagues-carryon byte-identical
      (9387 files both sides, 21m53s over SMB at ~11 MB/s).
      
      903 passed, 0 failed.
      PLN (Algolia) authored
    • feat(tools): rig-doctor — does THIS machine have what the rig needs at all · 58371350
      Answers a different question than gig-preflight.py's "is the installed rig
      HEALTHY right now": on a fresh box with nothing installed, can it even GET
      there, and for each gap, the exact command to fix it. Same spirit as fbk
      doctor — a friend who bought PLN's old LCXL2 can run this on their own
      laptop and paste the output back, turning "does this work elsewhere" from a
      guess into a list. Installs nothing; every check reads (binary --version,
      systemctl is-active/is-enabled, aconnect -l, git log, subprocess import
      probes) and nothing writes to the machine, the repo, or a service.
      
      The rule that matters most: never hardcode /home/pln or any absolute user
      path, and re-derive everything at call time (Path.home(), $XDG_DATA_HOME,
      Path(__file__).resolve()) rather than baking a module-level constant from
      the machine that wrote the file. That class of bug is real and already in
      this repo — start_and_midi.scd hardcodes three sample roots as literal
      /home/pln/... strings, so on a box where the user isn't pln, SuperDirt
      boots clean with those banks silently gone. This tool is built to be the
      thing that catches that, which means it can't commit it itself.
      
      Sample coverage deliberately does not parse .tidal files (one parser,
      tools/setlist_samples.py, per test_one_tidal_parser.py) — it shells out to
      tools/sample-pack.py --all --json and, with --bundle PATH, verifies against
      a carry-on bundle's MANIFEST.json as independent ground truth (without one,
      sample-pack can only self-report what THIS box already resolves, since
      resolving a bank name requires the folder to already exist on disk — a
      fresh box has nothing to walk).
      
      Findings from this run (PLN's own working laptop, the correctness signal —
      39 pass, 0 fail, 5 warn across all four invocations: human, --json, --quiet,
      --bundle /home/pln/Work/Sound/parvagues-carryon):
        - toolchain fully green: sclang/scsynth 3.14.1, all 6 quarks at their
          known-good commits, ghc 9.4.7/cabal 3.14.1.1/tidal 1.10.0, pulsar
          v4.1.0, ardour9, pipewire+pipewire-jack, all 5 python deps
        - all 281 carry-on bundle banks (15.11 GB) present when checked with
          --bundle; 18/18 synthdefs present
        - real, structural gaps this box legitimately WARNs on: /etc/sudoers.d is
          750 root:root, so presence of perf-audio's sudoers rule can't be
          verified without root (reported honestly as "unknown", not a false
          FAIL); no LCXL plugged in at run time; .env/yt-profile absent (this run
          happens to be from a worktree, which doesn't carry gitignored files —
          a checkout artifact, not a machine gap)
      
      python3 -m pytest -q: 903 passed (unchanged baseline).
      PLN (Algolia) authored
    • feat(rules): one parser for .tidal, enforced by a shrink-only ratchet · ff569232
      I wrote a THIRD .tidal parser this session -- an ad-hoc regex census of which
      synths the set calls -- while tools/setlist_samples.py already answered exactly
      that. It produced garbage twice: a substring grep matched the synth `gfunk`
      against the `gfunk_*` SAMPLE banks, then the regex version read `s "k"` (58
      files) as a missing sample and I reported it as a possible bug. PLN caught both
      in seconds. `#` is `|>` -- structure from the left, values from the RIGHT -- so
      `s "k" # s "jazz"` plays jazz and `k` is a rhythm skeleton that must never
      resolve. Nothing was broken.
      
      His diagnosis is the one worth keeping: a CLAUDE.md line alone would be a
      bandage, because the cause is not ignorance but COST ASYMMETRY. tools/ holds ~40
      scripts with no index and the canonical resolvers are human-facing CLIs, not
      importable functions, so writing a regex cost 30 seconds and finding the parser
      cost a search. The cheap path wins under pressure. What black teaches Python
      devs is not taste, it is that the decision is gone.
      
      So: a rule in CLAUDE.md that states the trap concretely (with the `s "k" # s
      "jazz"` example, since the rule is useless if you don't believe it), plus
      structural enforcement in tools/tests/test_one_tidal_parser.py.
      
      The test matches the SHAPE of a Tidal call-site regex, not a blocklist of
      filenames, so a new file is covered the day it is written. It is a RATCHET, not
      an amnesty: KNOWN_DEBT may only shrink. A file not on the list that starts
      parsing fails immediately; an entry that stops offending ALSO fails, so paying
      one off must be recorded. Mutation-verified in both directions -- a planted
      regex turns it red, and a fictitious paid-off entry turns it red -- because a
      green test is not evidence that it can fail.
      
      Enforcement immediately found FIVE pre-existing hand-rolled parsers I did not
      know about, plus a sixth once the markers were tightened. They are not the same
      mistake, so they are listed individually rather than waved through by
      directory: tidal_score.py, sample_tfidf.py and pattern_ngrams.py genuinely
      duplicate bank extraction and are what #23 should absorb; at/lens.py reads
      `mask "..."`, pvlint/rules.py reads `# n "..."` and deshadow-helpers.py rewrites
      whole source lines -- different concepts, and forcing those through a bank
      resolver would be the wrong API.
      
      Precision here is load-bearing: a looser first draft flagged
      tools/setlist.py's "^(sound\s*check|line\s*check|balance)$", which is about a
      stage soundcheck and has nothing to do with Tidal. A rule that cries wolf gets
      switched off.
      
      Also fixes a fake test inside the hollow-suite guard itself: test_suite_hygiene's
      `test_files` was a HELPER, collected by pytest as a test that passed by
      returning a list without asserting anything -- the exact shape that file exists
      to catch. Renamed to _gather_test_files. That was the suite's only
      PytestReturnNotNone warning.
      
      903 passed, 0 failed, 28 warnings.
      PLN (Algolia) authored
    • add(tools): sample-pack.py — bundle a set's must-travel sample banks · fa7aef0b
      Answers: for a chosen set of tracks, which Dirt-Samples banks does it
      actually need, and can that be handed off over swisstransfer to play the
      set from a different laptop? Selectors: --tracks (explicit/bare names),
      --set NAME (armada/setlist_*.txt or a gig's tracks.json under
      ../../Web/www/content/lives/{year}/{slug}/, --set list to enumerate),
      --all (every .tidal under live/+copycat/, the default). Dry run by
      default; --pack DEST copies, --manifest-only writes just the manifest
      (the one meant to be committed, so a remote box can verify/reproduce
      the bundle without re-scanning).
      
      Reuses tools/setlist_samples.py (extract_names/build_index/resolve_track/
      read_setlist/bank_file_count) as the ONLY .tidal parser — no new regex
      over .tidal text. A second ad-hoc parser is the specific mistake this
      tool exists to avoid (a prior grep false-positived "gfunk" prose against
      the gfunk_* banks).
      
      THE MAPPED-ONLY FILTER: only banks actually mapped into the Dirt-Samples
      root travel (a real dir there, or a symlink into ~/Work/Sound/Samples).
      Classified as stock (tracked in the Dirt-Samples git checkout — ships
      free with a fresh install, excluded unless --include-stock), mapped
      (the symlink farm + local additions), extra (~/Work/Sound/Samples/extra),
      or unmapped (e.g. tidal-drum-machines, excluded unless --include-unmapped).
      The raw ~/Work/Sound/Samples tree (36 GB, mostly work-in-progress) is
      never walked directly — only through the same mapped index
      setlist_samples.py already resolves against, which is what keeps that
      36 GB out of the bundle.
      
      BANK-INDEX SAFETY: `s "bank:N"` indexes files in the order SuperDirt's
      pathMatch("*") glob returns them, which skips dotfiles. So packing copies
      EVERY non-dotfile in a bank (never subsets), follows symlinks to copy
      real content, and preserves the bank's directory name and file names
      exactly — dropping even one real file renumbers every index after it and
      changes what plays on the receiving machine. --force on a re-pack rmtrees
      the destination bank first rather than merging, for the same reason: a
      merge leaves stale files from a previous pack sitting next to fresh ones.
      
      Measured against --all today (714 .tidal files, live/+copycat/): 400
      banks resolve; 119 are stock (~166 MB) and excluded by default; 265
      mapped + 16 extra = 281 must travel, ~15.1 GB total. rhadamanthe_divers
      alone is a symlink to 314 files / ~4.3 GB, roughly 30% of the bundle.
      
      Tokens that resolve to no bank (--unresolved) are NOT reported as
      missing/broken: ParVagues' `#` is structure-left/values-right
      (`$ s "k" # s "jazz"` — k is a rhythm skeleton, jazz is what plays), so
      most unresolved tokens are structural placeholders or built-in SuperDirt
      synth names (superpiano, moogBass, supersaw...), never dropped samples.
      setlist_samples.py's own docstring treats the unresolved set the same
      way — as a non-error remainder, not an alarm channel.
      PLN (Algolia) authored
    • docs(archive): ten completed tasks from the 09-05/06 session · 747f5be2
      Written as long-form learning rather than a list, per the archive's own
      convention — these are blog and video source material, and the learnings are
      the half worth keeping.
      
      The through-line of the session, visible only once the entries sit together:
      five separate failures where a CHECK was the broken thing, not the system it
      watched. The delete verifier reporting 0/14 for fourteen successful deletions.
      Six test suites green because they ran nothing. An assertion with an
      always-true escape clause. A drift guard that had been failing behind an
      --ignore flag people were trained to pass. A subagent's ten red tests that were
      its own stale checkout.
      
      Nearly committed this append on the wrong base: the shared checkout is on an
      older branch whose copy of this file is 162 lines behind master, so appending
      there and committing would have DELETED master's most recent entries. Extracted
      the block, restored the shared tree byte-identical, re-applied on origin/master
      in a worktree. Same lesson as the rest of the session — a stale read makes an
      append destructive, and the file being tracked is exactly what hides it.
      PLN (Algolia) authored
  3. 05 Sep, 2026 6 commits
    • docs(log): 045 — the master bus, painted behind the rain · 90c4270a
      Keeps the two things worth retelling: the A/B/A design that survived my own
      wrong premise about the xrun baseline, and Popen(bufsize=0) returning a raw
      FileIO whose short reads read as EOF — green selftest over a backdrop that
      could never receive a sample.
      PLN (Algolia) authored
    • feat(midiviz): the master bus, painted behind the rain · 504b0366
      PLN: "can we overlay behind a basic spectro? that id remove spectro VSTs
      from ardour and wed have our super perf one as overlay on single win?"
      
      So a coarse spectrum of the DEFAULT SINK's monitor -- the mix as the room
      hears it, not one orbit -- painted first, under every glyph, in the window
      that is already on top of everything. 40 log bands off a 2048-point FFT at
      18 Hz: scenery, not an analyser. If something needs measuring, tidal-ears
      is the tool and it is the right one.
      
      Off by default and off both ways. `--spectro` or the `s` key acquires the
      tap; `s` again hands it back -- the capture subprocess does not exist until
      asked and does not survive being un-asked, closeEvent releases it, and the
      unit deliberately carries no --spectro so login never acquires one. A live
      rig does not get to grow an always-on audio consumer quietly.
      
      Capture and FFT run in one thread whose entire contact with Qt is a single
      frame swapped under a lock, depth ONE: a GUI at 5 fps reading an analyser at
      18 fps shows the newest picture and discards the rest. A queue here would
      only buy latency, which is what a 512-deep queue already cost the HUD once.
      pw-record is asked for 100 ms latency explicitly -- a monitor client that
      requests a tight buffer is how you talk the graph's quantum down and pay for
      a decoration in xruns.
      
      Measured, A/B/A, same -30 dBFS 1 kHz bed in every window, 120 s each: 0
      added xruns with the tap on (2249 analysed frames, 18.7/s), 0 in either
      control window, every node's counter unmoved. Nothing in the graph noticed.
      
      Two things this cost, both recorded where they bit:
      
      * `Popen(bufsize=0)` hands back a RAW FileIO, so `read(n)` is one os.read
        and a hop-sized read is short far more often than not. Treating that as
        EOF ended the capture on its first chunk -- blocks=0, no error recorded --
        and every pure-function test stayed green on top of a backdrop that could
        not receive a sample. Only running it against real audio found it.
        `_read_exact` now owns the distinction, with a dribbling-stream test.
      * the first assertion about band placement was wrong about the code it
        tested: below ~750 Hz a 2048-point FFT has fewer bins than we have bands,
        so the edges are not the nominal log ramp, and checking 220 Hz against the
        ramp reported MISPLACED for a band that is exactly 211-234 Hz. Ask the
        edges where the band is.
      
      And the reference was wrong before the code was: the first bed I labelled an
      "80-226 Hz sweep" was 0.02*sin(2*pi*f(t)*t), whose instantaneous frequency
      is f + t*df/dt -- broadband chirp garbage. The spectrum was reading it
      correctly. A known 1 kHz tone lands in band 22, span 984-1148 Hz, every
      other band at 0.00.
      
      `_paint_spectrum` was checked against all 33 methods on the widget before it
      was named: `_paint_chrome` had to become `_paint_controls` because a second
      method quietly took the first one's name and the selftest went on passing
      while a whole layer stopped being drawn.
      
      selftest PASS (bars=40, tap released); 106 tests pass.
      PLN (Algolia) authored
    • chore(catalog): regenerate — the 8 new gigs reach the tracks that played them · a4ad0752
      The eight gig tracks.json files landed in 3a7c660b but the generated catalog was
      never rebuilt against them, so 36 tracks were still missing the gigs they
      appear in — ataright knew about bazurto and divin-live but not mephisteuf,
      and so on down the list.
      
      Rebuilt with build_catalog.py, not copied: the shared checkout had an
      uncommitted regeneration sitting in it, and this build reproduces it
      byte-identically from committed state, which is the only version worth
      trusting ([[feedback_parsers_over_copy]] — a catalog is an artifact, so
      regenerate it, never hand-carry it).
      
      Counts unchanged at 81 tracks / 7 authored overlays; this is purely the
      gigtrack edges filling in.
      
      Deliberately NOT committing catalog_view.json. Its clean rebuild here already
      equals master, while the copy in the shared tree differs in `takes` for 24
      tracks and in `source` for 3 — the latter because it was built with PLN's
      uncommitted .tidal edits baked into it. A generated file that embeds
      work-in-progress is not reproducible from the repo, so committing it would
      pin someone's scratch state as canon.
      PLN (Algolia) authored
    • docs(log): 044 — six suites that were green because they ran nothing · 6fb943a6
      The entry for the test-hygiene sweep, written while it is fresh. Keeps the two
      findings worth retelling: the assertion with an always-true escape clause that
      could not fail for months, and the collection error that trained everyone to
      pass --ignore and so hid a live API grading silence as tier A.
      PLN (Algolia) authored
    • fix(api): the live grader would score silence tier A — re-vendor, and stop · f1755c76
      hiding the suite that said so
      
      armada/api/engines/grade.py is a vendored copy of tools/foundry/engine/grade.py
      and had fallen three fixes behind the canonical:
      
        * the `empty_dbfs` presence gate. Every other sub-score is undefined on
          silence — the seam between two silences is perfect, the DC of silence is
          zero, its zero crossings are trivially fine — so a silent window scored
          0.90 x perfect and came out tier A. Fourier is deployed; this is what it
          was serving.
        * the seam denominator: mean |2nd diff| collapses to ~0 on sparse material,
          so a genuinely clean dub-drum loop measured 129x the mean and read as a
          huge click. The canonical uses the 90th percentile, amplitude-floored.
        * `not empty` on the low-RMS and mono-incompatibility flags, so silence stops
          being reported as maximally mono-incompatible.
      
      Re-vendored by copy after checking the diff both ways: the canonical is a strict
      superset and the only vendored-only lines were the OLD versions of the three
      changed ones, so nothing API-specific was lost. Now byte-identical, which is
      what the drift guard actually wants.
      
      The drift guard was not broken. It had been failing, in-repo, unseen — because
      of the second half of this commit.
      
      armada/api needs FastAPI, which lives in ~/.virtualenvs/fourier. Run `pytest`
      from the repo root with the plain interpreter and its seven test modules raise
      ModuleNotFoundError during collection, which pytest reports as "Interrupted: 7
      errors during collection" and then runs NOTHING — not the api suite, and not
      the other 890 tests either. The natural response is `--ignore=armada/api`,
      after which the api suite has no verdict at the root at all and can sit red
      indefinitely. It sat red long enough for the grader to drift.
      
      So conftest.py now skips that tree when fastapi is absent and prints the exact
      command to run it properly. An unrunnable suite should say it is being skipped
      and say how to run it; an error that stops everything only teaches you to pass
      a flag that hides it.
      
        plain python3 at root      892 passed,  2 skipped   (was: 0 run, 7 errors)
        fourier venv at root       993 passed,  2 skipped   — the whole workspace
        armada/api in its venv     101 passed,  0 failed    (was: 1 failed)
      PLN (Algolia) authored
    • fix(leds): five tests asserted a spec the board stopped using in August · 8dd2b710
      Master was red and nobody had noticed, because these five never fail alone —
      they fail in a full-suite run, and the habit had become running subsets.
      
      Four of them still described PLN's original six-step LED ramp, three of whose
      steps were dim. value_ramp moved to the DAYLIGHT ramp — five steps, every one
      full brightness — on his own ruling of 2026-08-21: "top brightness always would
      make more readable signals even in day perfs". The code carried that reasoning
      in its docstring; the tests were never brought along.
      
      So the tests now assert the shipped ramp, and the six-step spec keeps a test of
      its own under LCXL_DIM_RAMP=1, because it is still live behaviour for a dark
      stage — superseded as the default, not deleted.
      
      Two of them needed a different LENS, not a different number:
      
        * Monotonicity was measured as (green, red) ascending, which held only
          because the dim ramp never took red away. The daylight ramp walks amber ->
          yellow -> green by REMOVING red at full green, so red has to count
          downwards. Same property; the lens has to match the control.
      
        * The DJ filters' exemption from the ramp was tested by comparing the two
          functions at the centre value only — and the daylight ramp's midpoint is
          also 63, so the two coincided there and the test failed while the exemption
          was perfectly intact. A one-point comparison cannot tell a bipolar mapping
          from a unipolar one. It now asserts the exemption exactly (a DJ filter is
          painted by filter_colour at every one of the 128 values), that the two
          mappings disagree on 69 of 128, and that the bypass detent is narrow where
          the ramp's amber is wide.
      
      And one thing worth keeping: the old monotonicity test read
      
          assert ranks == sorted(ranks) or len(set(ranks)) == 6
      
      The six-step ramp has exactly 6 distinct ranks, so the second clause was ALWAYS
      true and the assertion could not fail. It was hiding a real fact — the dim ramp
      genuinely doubles back, dim red -> bright red then bright amber -> dim green —
      which is the measured reason the daylight ramp is the better default, not just
      a brightness preference. That is now asserted as the truth it is.
      
      The fifth was tools/at/tests/test_lens.py pinning the OPAL setlist at exactly
      13 tracks. That file is GENERATED from backlog.md, PLN's SSOT for set
      membership and order, so its length changes whenever he changes the set — it
      went 13 -> 16 when OPAL was recorded as-performed, and the test failed for the
      set doing exactly what it is supposed to do. A hardcoded count on a
      human-edited artifact is a stale binding. What the assertion is really for is
      non-vacuity (an unparsed setlist would make the loop below run zero times and
      pass), so it is a floor now, plus every path in the setlist must resolve on
      disk.
      
        before   885 passed, 5 failed
        after    892 passed, 0 failed
      PLN (Algolia) authored