1. 22 Sep, 2026 40 commits
    • docs(gig): the launcher had never launched, and Thursday morning's order · bc0aee3b
      Corrects the tooling-pass section's acceptance note: 'window closes silently =
      GO' described the bug, not the check. Adds the morning sequence (start the
      surface driver, press the icon once, read check-audio, then the 2h arc) and
      three learnings -- the positive-marker rule for any wrapper that closes on
      success, the wrong-port-draws-a-confident-wrong-picture class, and the fence
      that stops a profile repair becoming a silence.
      PLN (Algolia) authored
    • test(gate): the loud-probe assertion was reading the weather, not the table · bc721c6e
      It asked plan(audio=True) and then asserted a property of the probe table. The
      loud probe needs=('scsynth',), so the test passed with the rig up and failed
      with it down -- a verdict that depends on the box is the one thing a gate's own
      suite may not have. force=True asks the table the question the docstring says
      it is asking.
      PLN (Algolia) authored
    • midiviz: the whole surface has a resting position, and the rain says what to type · 64c29a05
      Two things PLN asked for, and one correction to how step 3 was planned.
      
      State covers all 48 cells now, not just the three family filters -- but only
      where the event layer cannot speak: a cell whose last event value equals the
      surface value is skipped entirely, track included. That keeps one fact to one
      picture, and what is left is exactly the gap an edge-triggered stream leaves
      behind -- a control untouched since launch, and a control the surface moved
      while this window was not listening. The family filters stay exempt, since a
      left-anchored bar lies about a centre detent whoever draws it.
      
      E needed nothing: the driver already owns the latch, because v3 DAW buttons are
      momentary, so it publishes 127 until the next press. This just draws it, which
      is why it cannot drift out of step with the board.
      
      The dirty-cell repaint the design doc was waiting on was the wrong screw. The
      widget repaints whole frames and its budget is made of drawText calls, not
      fillRects; the layer needed a rule about when NOT to draw, not a cheaper way to
      draw.
      
      And the gutter rains corpus tokens now -- ^42, the exact string a .tidal file
      types -- for controls the grid owns, where before only un-gridded CCs rained at
      all. Rate-gated per control, because the relative rows send a message per
      encoder click and an ungated sweep fills all 32 heads with one repeated word.
      The lanes are computed from the text width, so a three-glyph token in a
      five-glyph gutter cannot be drawn off the edge.
      PLN (Algolia) authored
    • launcher: a clean exit is not a launch, so stop letting one look like the other · 89d80cc7
      The only thing ~/.cache/parvagues/gig-up.log has ever contained is gig-up's own
      usage text. --help prints the options and exits 0, the window script saw a zero
      and closed itself, and the result was indistinguishable from a successful boot:
      a terminal that flashes and vanishes. Nothing had been started, which is how it
      was noticed at all -- the LCXL stayed dark, because no launch ever reached the
      paint step.
      
      The exit code cannot carry this; --help exiting 0 is correct. So the fence is
      whether the launcher reached its own last line: every real path prints
      'gig-up done', while usage, an unknown option and any early exit do not. On a
      zero with no marker the window now holds, names the symptom (no SuperDirt, no
      Ardour, no colours on the LCXL) and exits 2.
      
      --quiet is exempt by construction: ok() is a no-op there, because that mode
      exists so the Bridge captures only WARN/FAIL. The test suite holds both ends of
      the coupling, so rewording gig-up's last line fails loudly instead of turning
      every real launch into a held window.
      PLN (Algolia) authored
    • audio: a checker for the profile pin, and a fix that refuses to be a silence · 6b23c9fb
      Adds tools/check-audio.py and wires it into the gate as 'audio profiles',
      advisory: a card pinned to Pro Audio is a preference, not a reason to refuse a
      launch, but it IS the reason pavucontrol offers no Speaker / Headphones / HDMI.
      
      The ranking is a faithful port of WirePlumber's own findBestProfile, including
      its refusal to ever select pro-audio -- without that, a card someone pinned
      there would read as correctly set.
      
      --fix counts the sinks it would leave behind and stops at zero. On this box the
      textbook repair (clear the pin, take WirePlumber's pick) would have switched off
      the only four sinks present, since the laptop codec is pinned off and the USB
      interface is unplugged. A fix that trades a wrong profile for no audio is not a
      fix, and the fence has a test with that case in it.
      
      Also records a finding the write-up turned up: the config fragment in
      ~/.config/wireplumber/wireplumber.conf.d/ is 0.5 format on an 0.4.17 install, so
      neither it nor the DMIC guard written alongside it in July has ever been in
      force. The tool flags that class -- a file that looks exactly like protection
      and does nothing.
      PLN (Algolia) authored
    • docs(audio): only Pro Audio is a saved pin, and the laptop card lost its HiFi profile · 7808c63f
      Two causes, one symptom. WirePlumber's own policy script never picks pro-audio
      (it skips it by name), so both cards sitting on it can only come from the saved
      default-profile file -- which is consulted before the automatic pick and is
      therefore why it never self-corrects.
      
      The laptop codec is a different story: ALSA UCM still lists its HiFi verb and
      the kernel brought Jack Out / Speaker / HDMI up clean at boot, so the profile
      list PipeWire built at enumeration is what is short. That needs a device
      re-probe, not a setting.
      
      Names the risk before the fix: pro-audio is the profile that opens the capture
      PCM which hung the DSP in July, and the node guard written then is no longer on
      disk.
      PLN (Algolia) authored
    • midiviz: test the state layer's PIXELS, and name the zero-zone guess · 80f2115a
      Two gaps in the commit before this one.
      
      `--selftest` writes no state file, so `_paint_state` returned at its first line
      through all three themes and all four window shapes. The previous message cited
      that PASS next to the new layer, which reads as coverage it did not have: the
      drawing had executed in exactly one place, a throwaway script, which is how a
      whole layer stays green while never running. There is now a differential render
      test — paint offscreen, read the pixels back, assert WHICH SIDE of the detent
      filled: left for a low filter, right for a high one, neither for one at rest,
      and nothing at all in the five columns this slice does not own.
      
      Getting that test right took three attempts, all three failing on the widget
      rather than the layer. Asserting two x positions in a cell are equal is never
      safe here — the CRT texture draws vertical lines every few pixels — and the
      strip's TRACK spans the full cell, so "this pixel changed" is true across the
      whole row regardless of value. The version that holds blits the texture off (the
      one overlay not under test) and compares flanks, plus asserts low and high are
      mirror images of each other rather than merely different.
      
      Second: the zero zone was a bare `abs(val - 64) <= 2`. It is now ZERO_ZONE, at
      module scope, with the reason it is a guess written next to it — rows B/C are
      relative and the driver integrates with a clamp (`max(0, min(127, cur + d))`),
      so a knob swept to an end and brought back loses every click it spent against
      the clamp and settles NEAR 64 rather than on it. Too tight shows a sliver of LOW
      where his hand says ZERO; too loose hides a real nudge off centre, which is the
      worse failure because the layer exists to stop the picture lying about rest.
      Narrow to start, and it wants a hand on the board rather than another argument.
      
      Suites 529 passed, 2 failed (both pre-existing).
      PLN (Algolia) authored
    • docs: the receipt for everything gig prep deliberately parked · b9d83bcd
      Eleven items, each with the acceptance test it is waiting on and the reason it
      was left rather than done. Ordered by what it costs to carry, not by effort.
      
      The two with a real deadline are at the top: retiring tools/gig-up.sh, which is
      only still present because nothing is deleted before its replacement has run for
      real AND because it is the transcription check's reference; and folding
      gig_gate's scaffolding into gig-preflight, which was refused this week for
      buying tidiness rather than behaviour.
      
      Written now rather than after the gig because the reasons are what rot. The list
      of files is recoverable from git; 'we left this because a corpus rewrite with
      Pulsar open destroyed a rehearsal edit once' is not.
      PLN (Algolia) authored
    • midiviz: state as well as events — the DJFs know where they are resting · 8171325c
      "as it maintains a state, it should be 'not all same default state' when you
      look at it between two movements? like, the DJFs are at zero, in the Low, or in
      the High, and this doesnt really grasp from viewing the midimon"
      
      Correct, and it was not a theming problem. This module draws EVENTS, and makes
      recency a visual axis on purpose: an event's identity is its position, its value
      is a bar, its recency is brightness. So between two movements every cell decays
      to the same floor tint, and a filter resting at zero is the same picture as one
      parked at the top. No palette fixes that — an event stream carries changes, and
      rest is the absence of a change.
      
      A viewer cannot derive it either. The LCXL3 has no readback, and rows B/C run
      relative (the surface sends v - 64), so the hardware holds no absolute position.
      Exactly one process integrates those deltas and therefore owns the value, and it
      is not the viewer: lcxl3-driver's `self.values` is not a cache of something else,
      it is the only copy.
      
      So: two channels, because two physics. Events stay edge-timed off the wire; state
      is a level, published last-value-wins.
      
        driver   `~/.cache/parvagues/surface-state.json`, temp file + os.replace, on
                 the existing loop at 10 Hz, written ONLY when the snapshot changed.
                 Every failure swallowed: this is a courtesy to viewers, and a
                 read-only /home must never stop the faders reaching Ardour.
        midiviz  one stat() per frame; an unchanged `seq` means zero parsing and zero
                 repainting, which is the "pay once" property asked for. Absent,
                 unreadable or wrong-shaped file all mean "no state", and the window
                 draws exactly what it drew before this layer existed.
      
      Deliberately not a socket or a broker: ~32 integers, idempotent, no history
      worth queueing. A socket buys a connection, backpressure and a dead-consumer
      failure mode in exchange for nothing. `cat` is the debugger.
      
      First slice is the three family filters, drawn in the cells they physically
      occupy (C1/C2/C3) rather than a strip of its own — the layout does not move, and
      this module's founding idea is that identity IS position, so state belongs where
      the events about it are, which is where his hands are.
      
      ZERO IS THE CENTRE DETENT, 64, not 0: gDJF is an lpf and an hpf section both
      wide open at ch=0.5, so centre is bypass and both ends filter. The event bar is
      left-anchored, which is exactly the lie — a filter at rest draws a half-width
      bar that reads as "half of something". The state strip sits above it,
      centre-anchored, with the detent drawn: a bar growing left of centre is LOW, right
      is HIGH, bare detent is ZERO. Static at rest, no decay, no pulse — being legible
      when nothing has moved for a minute is the entire point.
      
      Found by looking at it rather than by reasoning: the first version drew into the
      bottom of the cell, where the event bar already lives, and C1-C3 rendered as
      mud. Checked at 900x560 and 1600x1000, and 3 units tall beat 2 in sunlight.
      
      16 tests, the load-bearing one being a real round trip: the real state_write and
      the real SurfaceState.poll over one temp path, so a change to either side that
      breaks the other fails here and not at a gig. That round trip caught a live bug —
      `null` is valid JSON of the wrong shape, and the AttributeError from the first
      .get escaped the handler and would have taken the window down.
      
      Suites 526 passed, 2 failed (both pre-existing); --selftest PASS across three
      themes and four shapes. Takes effect on the next driver restart; the running
      driver was not touched.
      PLN (Algolia) authored
    • docs(status): rig is one launcher and one gate, and pressing it is the SC restart · ce6ee670
      RIG gains the tooling pass and STAGE gains the daylight-readable monitor. The
      percentages do not move to 100: the launcher's new blocks have never executed,
      so 'never pressed' is the honest mood line and the press is a YOU row.
      
      Dropped 'Restart SC so 132 banks warm' as a separate item — the launcher now
      boots SuperDirt as parvagues-sc.service, so that IS the launch. Two rows for one
      action reads as two actions, and the next-3-moves list says it once, with what
      it buys: starts SC, warms 132 banks, runs 39 checks.
      PLN (Algolia) authored
    • docs(rig): the OOM daemon has been protecting all along — 21 writes, 0 EACCES · 7e6c6b27
      The archived note said install-protect.sh was never run and therefore "the
      OOM-protection daemon has never protected anything", reasoning that scsynth and
      sclang read ok only because they were already at the target so no write was
      attempted. That inference was sound and the premise was wrong.
      
      The unit is installed (Sep 6 23:30) and active since Sep 20, the installed copy
      carries CAP_DAC_OVERRIDE in both CapabilityBoundingSet and AmbientCapabilities,
      and the journal holds 21 protections with 0 permission errors over 7 days — each
      on a process that started at the default, which is the write the note said never
      happened: sclang[755981] oom:200->-1000, today at 15:04.
      
      Matters now because gig prep calls for restarting parvagues-sc to warm the
      banks, and the unit has no OOMScoreAdjust of its own: this daemon is the only
      thing between scsynth and DefaultOOMScoreAdjust=200. It re-protects within
      seconds, so the restart is safe.
      PLN (Algolia) authored
    • midiviz: the window resizes, the type follows it, and the theme is two clicks away · 3633fa0f
      Three complaints from the rehearsal, one root cause each.
      
      "unusable in the sun in light mode" — themes are now top-level rows in the
      right-click menu with painted swatches and a ● / ○ marking the live one, plus a
      `MIDI ▸` submenu in the tray carrying the same three. Sun is listed first,
      because the moment you need it is the moment you cannot read the menu to find
      it. The swatches paint from the Theme ramps rather than the live LUT, so the
      menu shows what you would get, not what you have.
      
      "only drag -> grab atm" — 7px edges and 20px corners hand off to
      startSystemResize, which is what a tiling WM wants; a manual setGeometry path
      covers compositors that refuse it, keeping the opposite edge pinned so the
      window grows where you pull. minimumSize 240x150, corner ticks brighten on
      hover, and leaveEvent puts them back (without it they stay lit forever).
      
      "have it squeeze and expand properly as we make it taller or wider" — the old
      `_apply_scale` resized the window, so scaling and being resized fought each
      other. Now `_fit` = min(w/ref, h/ref) clamped to [0.35, 2.4], multiplied by the
      density knob, and `resizeEvent` re-measures rather than rescaling: idempotent,
      keyed on the pixel sizes it actually produces, and no resize() inside a resize
      handler. The reference is the birth size read BACK off the widget, so fit == 1.0
      at birth and the look at 1.0x is byte-for-byte what it was. Key `0` resets.
      
      FIT_MIN went 0.55 -> 0.35 because 0.55 crushed the layout at 2.4x on a 240x150
      tile and printed rows on top of each other — found by screenshotting six window
      shapes, not by reasoning about it.
      
      Tray theme switching is stop -> forget -> launch, and the forget is load-bearing:
      two relaunches inside SPAWN_GRACE would otherwise close the lens and then refuse
      to reopen it. It returns early if the stop is not confirmed, so a wedged monitor
      is never followed by a second one.
      
      Variants live in BY_KEY only, never in LAUNCHERS or snapshot(), so they cannot
      be converged into existence behind your back.
      
      Verified on screen (grabs, scaling, colours) before this landed. 75 tests over
      the three modules; suites 510 passed, 2 failed, both pre-existing. `--theme` is
      still one-shot, so a converge brings dark back — inherited, not fixed here.
      PLN (Algolia) authored
    • fix(launch): boot SuperDirt as the unit, not raw sclang in a terminal · 4d8e3e2a
      Unifying the two launchers surfaced a divergence neither had been measured
      against. The root script span raw `sclang` under setsid; the Bridge's RIG UP
      went through the other gig-up, which started parvagues-sc.service. Repointing
      the button at one launcher would therefore have taken it OFF systemd.
      
      Raw sclang loses, all at once: LimitMEMLOCK=infinity and LimitNICE=-20 (which
      scsynth inherits), the 10-pipewire-jack.conf drop-in this script hand-rolls as
      \${wrap}, StandardOutput=journal + SyslogIdentifier (so the post window is
      parseable, and so the `SuperDirt warmed` probe — which keys on the unit's
      ActiveEnterTimestamp — can ever say yes), Restart=on-failure bounded 3-in-300s,
      and the OOM protection: systemd's user manager ships DefaultOOMScoreAdjust=200,
      and an unprotected scsynth was terminated for memory on 2026-08-15 after four
      crashes in one afternoon.
      
      None of this is new reasoning — it is parvagues-sc.service's own header, written
      2026-07-27: "SuperDirt as a managed unit rather than a thing running in a
      terminal somewhere". This launch path simply predates it, and the divergence was
      invisible because each launcher was the only one anyone watched.
      
      Keeps the 2026-08-01 distinction that cost an evening: a unit that is ACTIVE
      while scsynth is DEAD needs a restart, because start is a no-op on an active
      unit. Falls back to raw sclang, loudly, if the unit is not installed.
      PLN (Algolia) authored
    • fix(launch): a missing python3.12 leaves the rig up and unproven, not a held window · 7c0d40e3
      The gate call hardcodes python3.12 (mido is installed for 3.12 only, and a gate
      that quietly ran on 3.11 would fail the MIDI probes for the wrong reason). But
      propagating rc=127 from a missing interpreter would hold a terminal open over
      the stage for a reason that has nothing to do with the rig — which is already
      up by the time this runs. Report it, name the fix, let the launch stand.
      PLN (Algolia) authored
    • tooling: one launcher, one gate — and four bugs the split had been hiding · cd9d0d80
      Two verbs instead of three names. gig-up.sh DOES; tools/check-gig.py PROVES.
      
      The collision was not a fork. `gig-up.sh` (root, 555 lines) launches the rig;
      `tools/gig-up.sh` (620) proved the set; `tools/gig-preflight.py` (522) already
      proved the machine. Both gates took --converge and --quiet, so the two paths
      looked interchangeable and were not: the preload gate sat only on the path not
      pressed, the readiness report only on the path the Bridge does not call.
      
      The gate is now a TABLE, not control flow: 23 probes, each carrying its command,
      its fix, its severity and its layer. The bash pipelines are kept verbatim — each
      was written the day a specific failure was found and the pipeline IS the finding.
      gig-preflight already emitted {check,state,detail,fix} over OK/WARN/FAIL, so the
      machine layer joined through its own --json with no rewrite. 23 + 16 = 39 checks
      in one report, one exit code. Measured: 16.5s full, 3.4s --fast, 0.3s machine.
      
      Found while wiring it, each verified against the failing leg:
      
      1. The launcher rewrote preload.scd from setlist_opal2026.txt on EVERY launch —
         the Sept 6 set. Measured 132 banks -> 45, leaving 41 of Thursday's banks to be
         read off disk mid-set: the failure check-preload's own header calls "debuted
         as a crackle at the venue". The set is now named ONCE at the top of the
         launcher and read by both the plan generator and the gate. Generation also
         converges instead of clobbering — a plan that already covers the set is left
         alone, because a rewrite can only narrow it.
      2. `LCXL present` reported the desk PRESENT with the board unplugged: it grepped
         all of `aseqdump -l`, whose third column is the PORT name, and lcxl3-driver
         publishes a virtual output called "ParVagues LCXL3". The gate was matching our
         own driver. Now anchored on the client name (old: present, new: absent, board
         off the bus).
      3. `setlist compiles` proved backlog.md's 15 tracks, not the 17 being played.
         --setlist scopes the compile and preload probes to one gig's file, via
         setlist_samples.read_setlist — never a fresh regex.
      4. `SC -> Ardour` was gated on scsynth, so it printed NO-GO on a healthy
         --headphones rig where there is legitimately no Ardour. Preconditions are
         per-probe now, and an unmet one is SKIP, never FAIL.
      
      Also: gig-preflight's `midi surface` warned that mk3 LEDs were unimplemented,
      untrue since lcxl3-driver took over painting. Both generations exercised against
      a recorded graph. And `gig-log preflight` (the armed-global check — gMask sat at
      127 for 74 minutes while every cold check was green) lived only in the launcher's
      report where it could not block. It is a blocking probe now.
      
      Enforcement, because a rule in a docstring is not a rule:
      tools/tests/test_check_gig.py, 39 tests. No probe may mutate anything (read over
      argv, never the source text — a fix hint is SUPPOSED to say "systemctl restart");
      the fence has its own detector; no check may be lost from the port while the old
      script exists; the launcher may not grow assertions of its own and must end by
      calling the gate.
      
      Suites 510 passed, 2 failed — both pre-existing (fold-orbits' PipeWire regexes
      tripping the Tidal-parser ratchet, test_gearbox's dash fallback).
      
      tools/gig-up.sh is kept, unrun, with a superseded banner: nothing is deleted
      until its replacement has run on a real launch, and it is the transcription
      check's reference until then.
      PLN (Algolia) authored
    • docs: design the gig-up merge — two verbs, not three names · 8b58011b
      The obvious reading is that the two gig-up scripts are forks that drifted. They
      are not. gig-up.sh is a LAUNCHER (start things in order) and tools/gig-up.sh is
      a GATE (14 assertions over the repo and the saved session) — two different tools
      wearing one name. And a third, tools/gig-preflight.py, is already the gate over
      the live machine; its own docstring says '--quiet (for gig-up.sh)', so the gate
      was always meant to be called by the launcher and never was.
      
      So the merge is a separation, not a diff: gig-up does, check-gig proves,
      gig-up-window owns the terminal. Three entry points become two, each answering
      one question, and the launcher ends by calling the gate — which makes today's
      failure (a gate that existed but was unreachable from the path actually pressed)
      structurally impossible.
      
      Six phases, each independently revertable, and phase 0 is a harness because
      neither script nor the preflight has any test coverage today. Nothing is deleted
      until the phase replacing it has run on a real launch: a bad gig-up at a venue
      is not recoverable under pressure.
      PLN (Algolia) authored
    • docs(status): sound is green, rehearsal is the gate · a55d08e9
      gig-up restored the fader baseline on its own, so the bucket that was 20% and
      flagged as the critical path is now 85% with three ticks: baseline restored,
      all twelve orbits reaching master, Master at -0.5 dB instead of -30. What is
      left there is two judgements, not two repairs.
      
      The ribbon moves to REHEARSAL, because the full 2h run really is the only
      thing left that counts. Moves become: restart SC so the 132 banks warm, run the
      arc with Ardour recording, A/B the two crush buses that stopped trading. New
      storage key so ticks against the old moves cannot read as done.
      PLN (Algolia) authored
    • fix(launch): the guard that could not see Ardour, the gate that was in the other… · 65976d13
      fix(launch): the guard that could not see Ardour, the gate that was in the other script, the window that never closed
      
      Three things PLN hit in one launch.
      
      ARDOUR RELAUNCHED OVER A RUNNING ONE. The guard was
      `pgrep -f 'ardour-[0-9]'`, and `/usr/bin/ardour` is a BASH WRAPPER that execs
      the real binary out of /usr/lib/ardour8 — so for the first second of a launch
      the process is `bash /usr/bin/ardour …` and that pattern misses it entirely.
      Re-run gig-up inside that window and the second Ardour fights the first for the
      session lock. Now matches the wrapper AND the exec'd name, both bracket-tricked
      so pgrep cannot match the command line asking the question. Verified against
      the live box: neither pattern catches tidal-ardour-autoroute, which runs
      permanently and would otherwise have made the guard always true.
      
      THE PRELOAD GATE WAS IN THE SCRIPT NOBODY LAUNCHES. There are two gig-up
      scripts with separate histories: this one (528 lines, what parvagues.desktop
      runs) has the readiness gate, cut-gpu, the headphones fold and the fader
      restore; tools/gig-up.sh (620 lines, what the Bridge's RIG UP runs) has
      check-preload, --bt-off and check-boot. Neither is a superset, and both accept
      --converge and --quiet, which is exactly how it stayed invisible. So the path
      PLN actually presses never checked whether the set's banks are warmed — the
      failure check-preload.sh's own header calls "debuted as a crackle at the
      venue". Added here as REPORT ONLY: `--fix` regenerates a load-bearing boot
      file, and doing that at a venue is the landmine rather than the cure. Output is
      filtered to the verdict and any MISSING banks; the 66-name "warmed on purpose"
      roster is dropped, because at a venue the useful output is one line long.
      
      THE TERMINAL STAYED OPEN AFTER A CLEAN LAUNCH. `kitty --hold` was a real fence
      — GNOME's Terminal=true handler is not guaranteed to exist and the boot log is
      what you want when a launch goes sideways — but holding on SUCCESS left a dead
      window parked over the rig all night, and on stage every window that is not the
      music is in the way. tools/gig-up-window.sh tees the log to
      ~/.cache/parvagues/gig-up.log every time, keeps the previous one as .log.1, and
      holds the window only on a non-zero exit. A file beats a held window on both
      counts: it survives the window and it survives success. It is a separate file
      rather than a longer Exec= line because the desktop spec reserves
      `" $ & ; | < > ( )` inside Exec, so a pipeline with a conditional read has to
      be escaped twice and breaks silently on the third edit.
      
      Merging the two gig-up scripts is the right fix and is NOT a two-days-out job —
      design landing separately.
      PLN (Algolia) authored
    • fix(surface): wap was un-compacted by an editor save, and re-measure the hand counts · d80e023e
      TWO THINGS, AND THE FIRST ONE IS THE LESSON.
      
      wap.tidal never got its d7 -> d6. The compaction wrote it, then Pulsar saved
      the file from a buffer loaded before the compaction ran and put the old
      declaration back. My "0 moves left" re-plan was taken BEFORE that save, so the
      verification was true when it ran and false ten minutes later. Same shape as
      the third-pass bug, from the other direction: a conformance check is a
      measurement of a moment, and a moment with an editor open is not a state.
      PLN has now closed Pulsar, so both plans report 0 moves across all 703 — and
      this time nothing else is writing.
      
      Second: the orphan-orbits HAND_MEASURED table had gone stale, which is exactly
      what it is for. Five of its ten tracks moved d7 -> d6 (bombe_dj, wap,
      piment_bresilien, mafia_sans_serif, the_revolution_will_be_sampled); the other
      five already declared both, so compaction had nothing to close and they are
      unchanged — which is a check on the tool, not a coincidence. Re-measured with
      `grep -hoE '^d[0-9]+' | sort -nu`, deliberately NOT with orbits_of(), because
      the block's own docstring warns that a hand measurement quoting the thing it
      checks is a tautology.
      
      And the original ear report changed orbits without changing meaning. PLN heard
      crimewave survive vague_de_crime -> bombe_dj; bombe_dj now declares d6, which
      COVERS the orbit he heard and exposes the one below it. vague_de_crime runs
      crimewave on both d6 and d7, so the sound is the same and only the carrier
      moved. The test now asserts {7} and says why — the ghost is a property of the
      PAIR, which is the whole thesis of the tool.
      
      Suites: 456 passed, 2 failed, both pre-existing and unrelated (the fold-orbits
      tidal-parser ratchet, and test_gearbox's dash fallback from 6fb0f98d).
      PLN (Algolia) authored
    • docs(gig): the third pass, and what d9 was actually doing · c6acc450
      A conformance check run before the step that breaks conformance proves nothing
      — the compaction moved 150 orbits and left 202 of their controls behind, and
      the tool said '0 move' about a file that needed four.
      
      Also records the answer to PLN's own d9 note: its attack was riding d12's
      Ardour-learned level knob, and its mask shares d6's gate with no legal
      alternative, because d9 has no button slot in the grid at all.
      PLN (Algolia) authored
    • docs(status): the page tells tonight's truth, with SOUND as the gate · 1567a814
      Five buckets: SET 100%, RIG 95%, SOUND 20% and flagged as the critical path,
      STAGE 15%, REHEARSAL 40%. Six items carry a YOU badge because six of them are
      PLN's hands and nothing else.
      
      SOUND leads because it is the only thing that can silence the show: seven orbit
      faders sit at -inf in the saved session, and Ardour's meters are post-fader so
      that failure is indistinguishable from Tidal emitting nothing. The card says so
      in one line rather than trusting anyone to remember it.
      
      Every counter computes from the clock — a date-math suite asserts the header
      and all three stops across day-before, day-of and day-after, including "gig
      played" once it is behind us. Checked at 1440 and 420, and with localStorage
      throwing: the page still renders, which is the only state a status page must
      never fail in. New storage key, so ticks from the old move list cannot show as
      done against new moves.
      PLN (Algolia) authored
    • fix(surface): the controls follow the orbits — 202 moves the compaction left behind · ca21d6b6
      The compaction renames a `dN` declaration; it deliberately does not touch that
      orbit's knobs and buttons. So the moment 150 orbits moved, their controls were
      sitting on the column of an orbit that no longer existed there — d6 answering
      to column 7 in 84 files. The two tools are one job in two passes and only the
      second pass closes it.
      
      Found by accident, which is the part worth recording: ete_a_mauerpark reported
      "0 move" right after the compaction, then reported 4 the moment something else
      made me re-plan it. A conformance check run once, before the step that breaks
      conformance, proves nothing.
      
      202 moves, 84 files, 0 clashes. Re-plan now reports 0 across all 703. Button
      conformance 93.3%, no CC gained inside the Ardour-learned or gF ranges, and the
      third full silent-eval sweep diffed per-file against the baseline: zero verdict
      changes. The seventeen-track set re-validated on its own afterwards — all 17 OK,
      pvlint 0 errors.
      
      Also in here, from reading ete_a_mauerpark's d9 to answer PLN's "[confirm d9
      effects]" note: its attack was riding `^16`, which is A4 — d12's LEVEL knob,
      MIDI-learned to an Ardour track gain. One knob moved an Ardour fader AND d9's
      attack, the CC77-goes-to-silence class the driver header warns about. Moved to
      `^20`, idle in this track. Its `mask` on `^58` is d6's own gate and d6 IS
      declared here, so one button drives two orbits — left alone on purpose: d9 has
      no button slot in the grid at all, so that is gSel work, not a rename.
      PLN (Algolia) authored
    • chore(setbuilder): refresh the catalog after the 132-bank preload · a40eb470
      Warm counts move with the plan, so the cards were understating coverage for
      the seven banks the new set just added.
      PLN (Algolia) authored
    • docs(backlog): the AI ENGINEER arc, as PLN wrote it · 3c393dc1
      Seventeen tracks in three movements with declared BPMs, ~9 min each. This is
      where the set lives; armada/setlist_thu24.txt follows it.
      PLN (Algolia) authored
    • docs(gig): the evening's state — set settled, surface coherent, faders not · 2f182a6a
      The tracker now leads with the one thing no tool can fix from outside: seven
      orbit faders sit at -inf in the saved Ardour session, so from a fresh launch
      more than half the rig is silent and looks exactly like Tidal emitting nothing,
      because Ardour's meters are post-fader. gig-up already restores the baseline in
      the Ardour-closed window, which is why this is a checklist line and not a fire.
      
      It matters more tonight than this morning: the orbit compaction moved melodies
      onto d5 and d6, two of the seven.
      PLN (Algolia) authored
    • fix(live): two crush buses in the set were being fought over · ff084fad
      pvlint PV011, and the only four ERRORS in the whole seventeen-track set:
      
        love_first        crushbus 41 written by d4 AND d12
        cafe_bouillant    crushbus 41 written by d4 AND d11
      
      A bus is one control signal. Two orbits writing the same slot means the last
      writer wins, so the bass's crush amount and the melody's were overwriting each
      other every cycle — nothing errors, nothing goes silent, the two parts just
      never get the crush they ask for.
      
      Moved to the numbering the corpus already uses for those orbits: d11 -> 111
      (110/111/112 appear 12 times across live/), d12 -> 121 (120/121, 7 times).
      d4 keeps 41. Not invented — counted.
      
      This is an audio-graph edit, so it wants an A/B by ear before doors: both
      parts should now respond to their own knob independently, where before they
      traded. pvlint over the set is now 0 errors.
      PLN (Algolia) authored
    • feat(surface): no d7 while d6 sits empty — 150 orbits compacted · f6be5205
      PLN: "part of the migration also: no d7 when d6 is free. d7 often was sefcond
      melo, should be a d6 now d6 ids not a shitty mapped knob, but a proper column
      like others".
      
      That is the history exactly. d6's knob used to be somewhere useless, so a
      second melody went to d7 and d6 was left empty; since the column remap every
      one of d4-d8 owns a fader, a B knob and a C knob, so the reason is gone and
      only the habit remains. A set with a hole at d6 wastes a whole column.
      
      150 moves in 133 files. `d7 -> d6` is most of it; a lone melody with both
      slots free compacts to d5.
      
      THE BAND IS {5, 6, 7} AND IT IS DERIVED, NOT DECREED. An orbit number decides
      which family DJ filter and which family mute the part answers to. From the
      authored grid, d5/d6/d7 share both maps, so a move between them changes
      nothing about the sound — while d4 is the bass (gF2) and d8 is percussion
      (gF1/gM2), so compacting into or out of those would silently re-assign which
      filter sweeps the part and which button drops it. The tool computes the band by
      asking lcxl_grid which orbits agree with d6 on both families, so if the grid
      ever changes, the band follows instead of rotting.
      
      It refuses rather than guesses: 7 orbits carry an explicit `# orbit N`, which
      overrides the output orbit independently of the `dN` name, so renaming the
      declaration alone would move the pattern's identity and leave its AUDIO behind.
      Comments are never rewritten — a commented `-- d7 $ …` is an alternative to
      re-enable mid-set. Bus slots (`crushbus 71`) and `cut` groups stay put: a bus
      id is an audio-graph edit and a cut group is a choke group, not an orbit.
      
      VERIFIED: re-plan reports 0 moves left. silent-eval --seeded re-run over all
      703 files and diffed per-file against the pre-migration baseline — zero verdict
      changes, 703/703 joined. And the declaration multiset of every one of the 133
      files is a pure permutation inside the band, with nothing outside it touched:
      no pattern was dropped and no orbit was declared twice where it was not
      already. That last check exists because a naive compaction fusing two blocks'
      melodies onto one orbit would be silent, not loud — the later declaration wins.
      PLN (Algolia) authored
    • chore(live): wap — drop the duplicate d5, park a shift · 165abe8b
      PLN's edit from tonight, saved before he stepped away. The file declared d5
      twice ('v1 magic repeat' then 'v1 science of repeat') in one block, where the
      later declaration silently wins and the earlier is dead code — pvlint PV005.
      The dead one is gone, and a `(0.125 ~>)` is parked as a comment.
      
      wap is out of the Thursday set anyway ("too sample heavy"), so this is
      housekeeping rather than set work.
      PLN (Algolia) authored
    • feat(set): AI ENGINEER, seventeen tracks in order · fe4441de
      PLN settled it: "the set is ready i thinki for now i settled on … i can play
      longer/shorter, might add some, but a good setup for now". Three movements —
      START, NUJAZZ PARADIZE, FINALE COMEDOWN — 17 tracks over 120 min, ~9 each.
      
      The shape moved with his ear, not with the tooling: cyber_hump, wap and
      bombe_dj are out ("not adequate too sample heavy its a nujazz/chill dnb set"),
      and ten tracks came in that were never in a candidate list — salut_nu,
      love_first, take_5_drops, bain_electrique, haunted_house, chere_mireille,
      cafe_bouillant, cafe_tiede, force_motrice, revolution.
      
      All 17 pass `silent-eval --seeded`: every declared orbit emits. Ten of them had
      never been validated for this gig at all.
      
      His two in-file TODOs are carried into the comments rather than dropped:
      ete_a_mauerpark wants its d9 effects confirmed, and sept1 wants fixing to the
      new controller. "Liquid Nite TODO finish convert" is now done — that was the
      column remap, applied this evening.
      
      Declared BPM is in the comments, and the three tracks that declare no setcps
      say "inherits" instead of borrowing a number they never set.
      PLN (Algolia) authored
    • feat(midiviz): a sun theme, a menu, and a tray it can hide into · 949b9926
      PLN: "midimon unusable in the sun in light mode :') can we have a basic menu
      for midimon, maybe a tray presence when runnig, with scaling, theme, etc
      options ? to help sets in all context, robust, antifragile".
      
      Colours leave the paint loop and become data: three themes resolved once per
      change, so a frame still constructs no QColor and the 30/5 fps split is intact.
      The cut that made it work is INK versus SURFACE — recency (digits, bars, pens)
      against cell bodies, column glow and gutter — because the docstring's own
      invariant is that a mapped-but-idle cell must never read as a hardware fault.
      On dark that is a glow above black; on light and sun it is a wash below the
      page. A test asserts the floor tint differs from the page on all three.
      
      `sun` needed two passes and only a screenshot said so: the first render climbed
      to a saturated mid-tone panel and the digits lost the fight against it — light
      with fatter bars, not a sun theme. Flattened tint, near-black ink at every
      decay level, and a bold face, which outdoors is the cheapest contrast there is.
      The decayed state was shot on purpose, since "no subtle dim states" is exactly
      about the moment after a movement ends.
      
      Menu and tray are one QMenu, and every entry calls the same method the key
      does — no second code path, and no setShortcut, which would have taken the keys
      off keyPressEvent and out of the selftest's reach. Tray guarded on
      isSystemTrayAvailable and silently absent otherwise; a hidden window drops to
      idle fps and X/q/Quit still really exit.
      
      Config at $XDG_CONFIG_HOME/parvagues/midiviz.json, temp-file plus os.replace,
      and load cannot raise: missing, corrupt, not-a-dict, unknown-theme, NaN scale
      and unwritable are each exercised. A monitor that dies on a bad config file is
      worse than one with no config, and this one is meant for the middle of a set.
      
      Verified: bridge tests 105 -> 121 passing (the single failure is the inherited
      test_gearbox dash case, identical before and after); selftest PASS on all three
      themes; and dark proven pixel-identical to HEAD at four scales by rendering
      both versions side by side.
      
      NOT verified, and not to be claimed: the tray on a real desktop. Headless has
      no status area, so only the degrade path was proven. One known follow-up — a
      window hidden to the tray is still alive, so the hub's focus_window does
      nothing for it and the tray is the only recall path.
      PLN (Algolia) authored
    • fix(surface): every track on its own columns — the remap finished, 148 files · 3827b629
      PLN: "lets do the migration tbh i want to trust all tracks, lets indeed make
      that proper."
      
      The #94 remap had only ever been run against a setlist, so conformance was a
      property of having been listed, not of being a track. 685 moves across 148
      files, one simultaneous permutation per file, zero clashes.
      
      Button conformance, measured by tools/button-column-audit.py before and after:
      **2154/2729 on the orbit's own column (78.9%) -> 2544/2726 (93.3%)**. The
      remaining 182 are overflow — an orbit with more controls than the grid gives it,
      left in place under a FIXME pointing at gSel, because moving them would put two
      orbits on one physical control.
      
      VERIFIED, and this is the part that matters for trusting 703 files:
      
        * silent-eval --seeded re-run over the WHOLE corpus, 703/703, and diffed
          per-file against the pre-migration baseline in
          docs/2026-09-22-catalog-playability.tsv: **zero verdict changes**. Same 597
          playable, same 85 compile-fail, same 21 silent-orbit, and not one file
          swapped sides. Run at nice 19 / ionice -c3 so it could not preempt the live
          rig mid-rehearsal.
        * re-plan over all 703: 0 moves left.
        * not one CC reference landed in a protected range — 378 refs gained, none of
          them in the Ardour-learned faders (77-84), the A1-A4 level knobs (13-16), or
          the gF1-3 family filters (49-51). Checked as a multiset diff of every CC in
          every changed line, not by reading the tool's promise.
        * every effect BUS slot is byte-identical before and after (`crushbus 41` and
          friends): the tool rewrites quoted `"^NN"` refs only. So the 37 corpus
          pvlint errors, 36 of them PV011 bus-slot sharing, are untouched by this and
          were there before it.
      
      AND THE TOOL STOPPED VANDALISING COMMENTS. The overflow pass expands a hit to
      the whole chain segment, so the closing parens of a multi-line step are not left
      dangling — and that expansion swallowed comments inside the step. First corpus
      run: 18 lines grew a second `--`, three grew a third, and two of the tool's own
      FIXME warnings were commented out by the tool that wrote them. A warning the
      tool hides on its next run is worse than no warning. Fixed, the corpus reverted,
      and re-applied: the two diffs now differ in exactly those lines and nothing else.
      PLN (Algolia) authored
    • docs(midiviz): the state wire — one owner, two physics, pay once · 87a1f28a
      PLN asked for the rendering to show where things REST, not only what just moved,
      and asked that the wire not pay any cost twice.
      
      Written down before any of it is built, because the interesting part is not the
      drawing: state has exactly one legitimate owner. The surface has no readback and
      in relative mode sends deltas, so the driver's integrated value is not a cache
      of something else — it is the only copy. A viewer cannot derive it, and at boot
      there is nothing on the wire at all.
      
      The design: level and edge as two channels, state as last-value-wins in one
      small atomically-replaced file with a seq counter so an unchanged frame parses
      and paints nothing, and dirty-cell repaint so the layer costs nothing at rest.
      Explicitly NOT built: a fan-out daemon for the duplicate aseqdump subscriptions,
      which buys microseconds at the price of a process and a failure mode.
      
      Two findings worth the doc on their own. Zero on a DJF is the CENTRE detent, so
      the readout wants three zones with the detent drawn, not a 0-127 bar. And the
      Pulsar-style feed needs no new producer: ~/.cache/parvagues/eval-events.jsonl is
      already being written, one line per ctrl+enter with the orbits it touched, and
      the driver already reads it.
      PLN (Algolia) authored
    • fix(setbuilder): stop inventing a tempo for a third of the catalog · 2baef681
      `declared_bpm` defaults to 120 when a track sets no setcps, which is right for
      the driver — the OLED has to breathe at some rate — and wrong for a picker.
      221 of the 703 tracks declare no setcps at all, so a third of the cards were
      showing a confident 120 that came from nowhere. That is the worst shape a
      wrong number can take: plausible, and load-bearing for the arc he is about to
      order tonight.
      
      Now the generator passes default=0 and those cards read "inherits bpm", which
      is not a gap but the actual behaviour — a track with no setcps runs at whatever
      tempo the previous one left behind, and for building a ramp that is worth
      knowing. They sort last on the BPM axis, since they have no place on a ramp
      until you decide what they inherit.
      PLN (Algolia) authored
    • feat(setbuilder): declared bpm and sample weight, because the set changed shape · 9f48601e
      PLN, at the desk: "cyber hump and wap and bombe dj not adequate too sample
      heavy its a nujazz/chill dnb set i think is the right one."
      
      That is an ear judgement the tooling cannot make, but it can hand him the two
      handles the judgement needs. Every card now carries the BPM the track declares
      and how many distinct sample banks it pulls from, and the list sorts by hands
      first, lightest on samples, or slowest BPM.
      
      Bank count is a proxy and it is labelled as one — it is not busyness or
      loudness, just breadth of folders. It does separate the three he named
      (wap 11, cafe_glace 10, bombe_dj 9) from the light end, which is the job.
      
      BPM is DECLARED, never measured, and the legend says so: tempo-lens.py opens
      by warning that setcps is not the tempo, since a track at setcps 80 with
      everything on the half-note performs at 160. Right handle for ordering an arc,
      wrong one for claiming a speed.
      
      `declared_bpm` goes into setlist_samples.py, where corpus syntax belongs, not
      into the generator — a second private regex for the corpus's own idiom is how
      the parser rule gets broken one convenience at a time. tools/lcxl3-driver.py
      carries an identical `parse_bpm`; it predates this and it is load-bearing two
      days out, so the duplication is recorded in a comment and collapses after
      Thursday rather than tonight.
      PLN (Algolia) authored
    • chore(live): sunny_side_up — acid bass up two octaves, the ^59 mask gate off · 26f20c02
      Desk edit from tonight's rehearsal. `|- note 36` -> `|- note 12` on acidOto3092,
      and the mask(16,32)+ply gate commented out so d7 runs the arp clean.
      PLN (Algolia) authored
    • docs(backlog): clear the tail, keep what was in it · c2fd9716
      PLN: "deliberate killed tail was just a agent log or srth, capture that removed
      as learning, nut then let me clean that backlog indeed was gboing tedious."
      
      The tail is cleared and the new AI ENGINEER candidate section stands. But the
      543 lines were not a log: they were two sessions of rig notes from 6-7
      September, and six items in them were still open. Losing them to a backlog
      cleanup is the quiet kind of loss — nobody misses a note they have forgotten
      writing.
      
      So the whole block is archived verbatim in docs/, with the open items hoisted
      to the top of it where they can be read without scrolling. The one that
      matters: `sudo tools/install-protect.sh` was never run, so the OOM-protection
      daemon has never actually protected anything — it reported ok only because
      scsynth and sclang already sat at the target value, and no write was attempted.
      PLN (Algolia) authored
    • docs(gig): where the hands have to go after the remap, and two stale claims fixed · 092e1a17
      The remap commit carried counts. Counts do not help at the desk: a knob that
      moved from C5 to B4 is a knob that does nothing when you reach for it. The
      tracker now holds the was/now table for every one of the 24 moves, because
      PLN is rehearsing jeudrill and rose_rouge tonight and both of them moved.
      
      Also recorded: techno_orage's ^58 firing across d5 and d6, the corpus-wide
      154-file job that is explicitly NOT for tonight, the 114 duplicate FIXME lines
      left behind by the old non-idempotent tool, and the fact that "the set" still
      resolves to the Opal list through backlog.md while Thursday's arc lives only
      in armada/setlist_thu24.txt — a default nobody should trust this week.
      
      Two fixes in the tool itself:
      
        * the FIXME dedupe was a snapshot taken before the insert loop, so one run
          could still stack two identical notes when an orbit is split across two
          blocks. It now learns as it writes.
        * the docstring still promised that buttons were "Phase 2, after the gig".
          They were folded in when #94 shipped, and the tool has been moving BT/BL
          cells ever since. A docstring that defers work the code already does is
          the one place a reader can learn the wrong scope.
      PLN (Algolia) authored
    • fix(surface): finish the column remap on the tracks actually in play · d8effb67
      The #94 remap was run against a setlist, not the catalog, so conformance was
      never a property of a track — it was a property of having been on that list.
      Two days of desk edits then pushed listed tracks back out: swapping jeudrill's
      d9 and d10 bodies moved the orbits but not their knobs, so d4's effect was
      sitting in d5's column again.
      
      Migrated, one simultaneous permutation per file, 24 moves across six tracks:
      
        jeudrill        6   the d9/d10 swap's knobs follow their orbits
        liquid_nite     4   the four PLN had not reached by hand
        rose_rouge      3
        techno_orage    9
        cyber_hump      1
        siberie_samuel  1
      
      liquid_nite is the one that prompted this — PLN saw it in the catalog, wanted
      it in the set, and found it half-migrated: he had walked the buttons down a
      column by hand (^42 -> ^41, ^43 -> ^42) and stopped. The knobs are now on
      their own columns too, and it evaluates clean.
      
      VERIFIED, not assumed: re-plan reports 0 moves on all six; silent-eval
      --seeded is OK on five. techno_orage still FAILS, on the same d2 that was
      already silent before this commit — unchanged, and still PLN's call to fix or
      cut. pvlint: 0 errors, 21 warnings, all pre-existing shapes (PV013 missing
      family mutes, PV005 double declarations). One is worth reading: PV008 says
      techno_orage's ^58 drives d5 AND d6, which is the overflow the tool annotated
      rather than commented — a $-step cannot be commented out without breaking the
      block's arity, so that button really does fire across two columns until gSel
      folds it.
      
      AND THE TOOL NO LONGER TALKS TO ITSELF. Every run stacked another
      byte-identical FIXME paragraph above the same line: cafe_glace carried the
      same note three times, and 90 files in the corpus carry 114 duplicate lines
      between them. An overflow is a standing condition, not an event, so it earns
      one note. The comment-out still runs on a repeat pass — if a line came back to
      life it is firing across columns again whatever the note says — only the note
      is deduplicated. Proven by applying twice and diffing: 15/13 both times.
      
      Corpus-wide, 154 files still want 709 moves with zero clashes. Not tonight.
      PLN (Algolia) authored
    • chore(live): commit the desk edits that were sitting uncommitted · 0399c2d3
      Everything here was already in the working tree — played, kept, never
      committed. Written down now so the gig runs off the repo and not off one
      laptop's dirty tree.
      
        siberie_samuel  NEW FILE, and it is in the Thursday candidate arc. It
                        existed only as an untracked file: nothing outside this
                        filesystem knew the track was real.
        liquid_nite     the buttons walked down a column by hand (^42 -> ^41,
                        ^43 -> ^42) and a long tail of commented-out orbits was
                        cut. The button axis is Phase 2 of the #94 remap, so this
                        is hand work ahead of the tool, not a conflict with it.
        jeudrill        d9 and d10 swapped bodies; the d9 crushbus commented out.
        you_my_sunshine cut 3 and the legato sweep off; room 0.14 -> 0.8, sz 0.9.
        techno_orage    back to the Children Holiday line, slow 4 on the notes,
                        cut 3 dropped.
        esperluette     the bassWarsaw d5 removed.
        sept1           resetCycles dropped from the top.
        piment_bresilien whitespace.
      
      setlist_opal2026.txt loses its SOUNDCHECK section, so the pairwise walk in
      orphan-orbits.py starts on the real opener. That file is a record of a past
      gig, so the removal is visible here on purpose rather than folded into a
      larger commit.
      PLN (Algolia) authored