diff --git a/docs/sprints/audio-upload-followups-2026-10.md b/docs/sprints/audio-upload-followups-2026-10.md new file mode 100644 index 0000000..38c9690 --- /dev/null +++ b/docs/sprints/audio-upload-followups-2026-10.md @@ -0,0 +1,88 @@ +# Sprint: audio upload follow-ups (Sweep Sprint, 2026-10) + +Method: Sweep Sprint (phase 1 parallel read-only research per issue: **done**; phase 2 sequential +execution, one issue per commit: **not started, waits for the owner's go**). Closing oath below. + +Milestones: [v0.31.1 - security and robustness](https://git.d-ma.be/mathias/tapir/milestone/10), +[v0.32.0 - audio upload follow-ups](https://git.d-ma.be/mathias/tapir/milestone/11). + +Context: v0.31.0 shipped audio upload (ADR-033/034/035) and is live in production behind auth at +`/uploads`, with the Berget cloud fallback on for audio (infra#522). A real 63.7 min upload +transcribed in 2m53s with no errors. The phase-1 investigation found two pre-existing or new +weaknesses an authenticated user can reach, so the first release is a hotfix. + +## Oath (closing gates, every clause must hold; blocked = descoped and tracked, never "done") + +| # | Clause | Evidence | +|---|--------|----------| +| SG1 | Every issue in the sprint is closed with a linked commit, or moved out with a stated reason | issue list of both milestones | +| SG2 | Each fix has a test that was seen red on main first, named after the mechanism it observes | red/green output in the issue | +| SG3 | `task check` AND `golangci-lint run ./...` exit 0 (checked by `$?`, not by grep); `task check` does not gate lint until #41 | gate output | +| SG4 | Control code (#48, #49, the upload cap in #47) has a recorded adversarial review verified by execution, verdict in the issue | gatekeeper verdict comment | +| SG5 | Docs current in the same commit: ADR-033/035 text, feature file, use-case coverage test | diff | +| SG6 | Each release tagged (v0.31.1, v0.32.0), pushed, deployed, and observed live (pod running the new image, no restart loop, a real upload still works) | `kubectl`/log + one upload | +| SG7 | Learnings written to the brain; deferred work filed as issues; decisions that are the owner's recorded as DECISION NEEDED | brain ids + issue numbers | + +## Work order + +### v0.31.1 (security and robustness; hotfix, do first) + +1. **#41 lint gate.** `task check` swallows lint findings (`|| echo`). Make the gate fail on findings, + fix what it surfaces. First, so every later gate means something. (Decision D1.) +2. **#48 cross-origin POST protection.** `http.NewCrossOriginProtection()` around `Router()`. + Red: foreign-origin POST to `/account/delete`, `/uploads`, `/uploads/{id}/chat` is refused. +3. **#49 ffprobe/ffmpeg limits.** `-max_streams 32` + narrower probe output; probe stdout cap; + probe deadline. Three commits. +4. **#47 concurrent-upload cap (remaining item).** Bound the spool: reservation per accepted upload + against the 6Gi emptyDir so concurrent uploads cannot evict the pod. +5. **#50 SIGTERM interim.** Handle SIGTERM; record the interrupted job at once; fix the false + comment. Full drain recorded as a limitation. (Decision D2.) + +Release v0.31.1: one adversarial review round over 2-5, then tag, push, observe. + +### v0.32.0 (follow-ups) + +6. **#52** label a cloud-served transcript in plain words; document the empty-transcript trigger. +7. **#53** bound the LLM client's response body read. +8. **#51** tracker: save timeout, then app-clock heartbeat. +9. **#37** nav link to uploads and per-recording delete (queued/transcribing: refuse until it + ends). (Decision D3.) +10. **#36** chat default model after measurement. (Decision D4.) +11. **#38** measurements (Berget length limit, large models on production iguana). (D5, D6.) +12. **#46** remainder of the hardening list after the items above are split out. +13. **#45** evaluate Berget as the reconciler: a question, not work. (D8.) + +## DECISION NEEDED (owner's call; recommendation first) + +| # | Question | Recommendation | Blocks | +|---|----------|----------------|--------| +| D1 | #41: should any linter be allowed to opt out of the gate? | No opt-out; fix findings | #41 | +| D2 | #50: full SIGTERM drain, or record as a limitation and ship the interim fix? | Limitation + interim; a drain costs 4-6 h and downtime on every deploy at current volume | #50 | +| D3 | #37: delete of a queued/transcribing recording | Refuse until it ends | #37 | +| D4 | #36: make a 31B model the chat default? | Only after measurement, conditional | #36 | +| D5 | #38: Berget length measurement needs a throwaway key | Yes, throwaway key | #38 | +| D6 | #38: load large models on production iguana | Yes, in a quiet window with the owner present | #38 | +| D7 | #46: where the ffmpeg tarball lives | Do nothing now; file an issue | none | +| D8 | #45: evaluate Berget as the reconciler | No; stay local-only | #45 | +| D9 | Who judges ADR-034's second blind check | The owner; do not reuse the one-off Opus authorisation | ADR-034 | +| D10 | When to enable reconcile in production | Needs `iguana/pianissimo` in the key pin and env vars; owner present | ADR-034 | + +## Dropped or reduced, with reasons + +- **#46 item 1 (`alis`/`dref` external reference):** checked, not a problem. Under the application's + flags ffmpeg skips the external track and writes a 78-byte WAV. +- **#46 item 2 / #47 item 2:** merged into #49. +- **#47 item 3 (full drain):** reduced to #50 plus a recorded limitation. +- **#46 `Beat` taking the tracker mutex:** incorrect in #46; only `logStill` does. Corrected in #51. + +## Human-present items (do not run unattended) + +- Production iguana model loads (#38, D6). +- Enabling reconcile in production (D10). +- Any key change or `apply` in infra (infra#523, infra#524 are open and not part of this sprint). + +## Release plan + +SemVer: v0.31.1 patch (fixes, no new behaviour); v0.32.0 minor (nav link, delete, cloud labelling). +Every push to tapir main redeploys the pod and fails any job in flight: push when no upload is +running (check the pod log first).