# The Foundry — TODO

Resume state for the URL→audio→stems→loops sampling tool. Snapshot **2026-08-21**.
Task IDs map to the session task board. Design: memory `project_foundry`.

## 🔵 In progress
- [ ] **#19 — Auto-tune loop** (create samples → grade → iterate sampling settings).
      PLN's closed-feedback main quest. **Harness BUILT & VALIDATED** (commit
      `6f85012`): `engine/autotune.py` (evaluate/search), `autotune.py` CLI,
      `loops.analyze_stem` `weights` override, 4 unit tests + a real run (baseline
      0.690 → best 0.763, **+10.5%** on 2 drum stems). Finder defaults (`loops.W`)
      deliberately UNCHANGED. **Exact next steps to resume cold:**
      1. Get real fuel: `cd tools/foundry && python3 foundry.py demos --all --sep`
         (fetches+separates the 10 sampling classics — SLOW, run as a durable bg
         job). Existing fuel: `~/Downloads/separated/htdemucs/*/{drums,bass,vocals,
         other}.wav` (27 tracks).
      2. Add the anti-gaming 2nd objective: recall-vs-provenance-GT (reuse
         `armada/tide-table/build_finder_eval.py` + `build_provenance.py`) so we
         don't just amplify the seam/zc confound. Make objective multi-objective
         (grade × recall) in `engine/autotune.py`.
      3. Per-stem-ROLE tuning: vocals want chop mode + different weights than drums
         (`--stems …/drums.wav` vs `…/vocals.wav`; analyze_chops path for vocals).
      4. Full campaign: more stems (`--max-stems`↑) + more configs (`--n`↑); factor
         window-feature extraction out of `analyze_stem` so weight sweeps are
         instant (extract-once/score-many — the scarcity-minded speedup).
      5. THEN adopt a VALIDATED profile as the new `loops.W` default, per role,
         with the lift numbers.
      Run: `python3 autotune.py --stems <glob> --n 24 --max-stems 4`.
      Objective so far = mean full-rubric grade × coverage.
      **VERIFY-RERANK now BUILT & validated end-to-end on the Super freak dub kit.**
      The finder scored the seam on the RAW window, but export zc-snaps (B1) — and the
      snap (plus the 3-dp start/end rounding export slices at) changes the wrap. Several
      proxy-seam≈1.0 windows CLICKED after the snap. Fix: `loops._verify_rerank` re-scores
      the ~3×top_n survivors' seam on the TRUE post-snap window (reusing `grade.seam_score`,
      DRY), swaps proxy→true seam in the composite, and hard-demotes true-seam <
      `VERIFY_SEAM_FLOOR`. New kwarg `analyze_stem(..., verify=True)` (default on; False
      reproduces the pre-#19 raw-proxy ranking for autotune baselines). Also fixed the
      sparse-material seam false-positive (`grade.seam_score`): the mean-|2nd-diff|
      denominator collapsed to ≈0 on sparse dub drums (129× click ratio on a clean loop);
      now 90th-percentile, amplitude-floored (2–3× on the same loop). Validation
      (drums+other, bars 1/2/4, BPM 132.5, export-faithful grades):
        drums top-3  BEFORE C/C/A  →  AFTER **S/S/A** (clicking @93.08 & @24.06 demoted)
        other top-3  BEFORE B/B/B  →  AFTER **S/S/S** (bad favorite @56.74 demoted out)
      +2 rerank regression tests + 1 sparse-percussion test; suite 57→60 green.

## 🟢 Landed (2026-08-21) — #20 batch, via producer stem packs

`engine/stempack.py` + `engine/roles.py` + `engine/kitcheck.py` + `kitgate.py` +
`restage.py`, branch `claude/fred-kits`, 14 commits, suite 86 → 112.
Full write-up: `armada/tasks/027-the-label-and-the-lens.md`, memory
`reference_stempack` + `feedback_label_and_lens`.

```bash
cd tools/foundry
python3 -m engine.stempack "<pack>" --jobs 9 --no-link --out /tmp/cut
python3 kitgate.py /tmp/cut/fred_kits.json            # then --link
```

## 🟡 In progress — finish the Fred pack (RESUME HERE)

The **3rd/final full run** was launched 17:32 (`pid 214097`, ~30 min, 9 parallel
children) writing to
`/tmp/claude-1000/-home-pln-Work-Sound-Tidal/51ee4fa8-7503-47a3-a4e5-cf19e930564b/scratchpad/final`
with log `…/final.log`. Runs 1 and 2 (161 and 153 samples) were DISCARDED — each
predated a fix. If that scratchpad is gone, just re-run the command above with
`--out <fresh dir>`; it takes ~30 min on 9 cores and is fully deterministic.

**Exact steps to finish, in order:**
1. `python3 restage.py --cuts <out>/fred_kits.json --dry-run` — expect **0 kits**
   changed, because run 3 stages at export. Anything else means the export path
   regressed.
2. `python3 kitgate.py <out>/fred_kits.json` — must be all-green on grade / kitcheck /
   bank shadowing. `rights` is advisory and WILL warn (correctly) until step 3.
3. `python3 ../../armada/tide-table/register_fred_rights.py <out>/fred_kits.json`
   — registers every `fred_*` bank as `third_party` **with provenance**, so a per-track
   release blocks by design. Do this BEFORE linking.
4. `python3 kitgate.py <out>/fred_kits.json --link` — the Dirt-Samples symlinks
   (PLN asked for this explicitly). Then `python3 ../bank-check.py --since 1` to confirm
   0 shadowed.
5. Copy `fred_kits.{json,md,tidal}` + `kitcheck.md` into
   `tools/foundry/packs/fred/` and commit (see `packs/README.md` — manifest, not audio).
6. Report to PLN: per-kit tier histogram, distinct-sound count, the "worth a second
   listen" table, and the paste-ready `n` indices.

**Expected shape** (from run 1/2, so approximate): ~40 kits, ~150 samples, ~9 tracks.
Tempo confidence gates one track (PLEASEMAKEITBETTER, 20%) to chops-only — correct.
MAREA is the ground truth: grid must read **123.0 bpm vs its declared 123**.

## 🔴 Open
- [ ] **#21 — The kit auditioner SPA (asked for 2026-08-21): browse + loop + sequence.**
      PLN: *"spawn a reusable SPA so i can listen to the samples, try each as a loop,
      quickly sequence — could be a strudel playground, i'm open."* Design facts already
      established, so this can start cold:
      - **Where:** a new page in `armada/ui/` (Vite + React 19 + TS + Tailwind 4), which
        ALREADY depends on `wavesurfer.js` + `@wavesurfer/react` and already ships
        sibling pages this way (`bounds.html`, `judge.html`, `sextant`). Follow that
        pattern; design language = the Ship's Bridge (`armada/DESIGN.md`), manage with
        the `impeccable` skill from `armada/`.
      - **Serving the audio:** add a `samples` prefix to `armada/ui/audio-mounts.json`
        → `/home/pln/Work/Sound/Samples`. That file is read by BOTH `vite.config.ts`
        (dev) and `armada/serve.py` (prod), so `/audio/samples/<kit>/<file>.wav` works
        identically in both. Same-origin, so no CORS work.
      - **CANNOT be a published Artifact.** The Artifact CSP blocks external hosts and
        the sandbox cannot reach a localhost audio server, so the page must be served by
        `armada/serve.py`. Don't waste a session rediscovering this.
      - **Sequencing = Strudel**, which is Tidal syntax in the browser, so a pattern
        built there is nearly copy-pasteable into a `.tidal` file. `npm i @strudel/web`;
        `initStrudel()`, then `evaluate("<pattern>")` / `hush()`. Load the kits with
        `samples()` pointed at a generated `strudel.json` map — write a small generator
        from `fred_kits.json` (it already carries kit, path, bars, bpm, tier, family,
        stem_role, dur_s and CLAP tags, so the browser needs no analysis).
        PLN's own research notes: `~/Downloads/Summarize how to integrate strudel js
        music framew.md`.
      - **The data is already browsable:** filter by track / family / tier, sort by
        grade, show CLAP tags, and surface the report's "worth a second listen" rows.
      - Suggested split: left = kit list, main = sample rows (waveform, tier badge,
        bars@bpm, loop-toggle audition), bottom = Strudel pane with click-to-insert
        (`s "fred_marea_drums" # n 3`). Confirm the layout with PLN before building deep.
- [ ] **#22 — Point stempack at the rest.** Same driver, no new code:
      the 13 ACAPELLAS + 13 INSTRUMENTALS in `~/Work/Sound/Samples/Fred/` (already
      44.1 kHz, so no resample; acapellas want chops mode), and the ~27 demucs catches in
      `~/Downloads/separated/htdemucs/`. Each needs a `--prefix`.
- [ ] **#23 — Run `kitcheck`/`restage` over the whole existing corpus.** The dead-bar and
      periodicity tests are general, and `Samples/` has 163 kits that have never been
      audited; hand-cut kits also carry whatever level their source had.
      `python3 restage.py --all-under ~/Work/Sound/Samples --dry-run` is the first look.
      Expect "grades S, useless" to be non-rare.
- [ ] **#20b — Batch-explore the merged corpus** (the original #20 framing).
      Workflow is one-catch-at-a-time; build batch exploration:
      - Batch driver: `find_takes` over MANY source stems (the ~30 in
        `~/Downloads/separated/htdemucs` + new fetches) → grade → surface the best
        loops/chops corpus-wide ("what's worth sampling across everything pulled").
      - **Stem-role-aware**: skip near-silent stems, mode per role (chops for
        vocals, loops for rhythmic), rank by grade. (e.g. Loituma has no drums —
        bass + high voice are the interesting stems; grid-from-drums already falls
        back drums→bass→first.)
      - Surface in the Hall of Fame / a "fresh catches" view: best ungraded-yet
        candidates corpus-wide, click to audition + forge.
      - Ties to #19: once settings are tuned per role, batch-explore applies them
        at scale. Relates #11 tierlist (showcase pattern), #9 merge.

> Note: the Foundry engines are also productized over HTTP by the **Fourier**
> audio API — `/separate` (#30) wraps `engine.separate`, `/loops /grade /correlate`
> (#31) wrap the rest. See `armada/api/TODO.md`.
