diff --git a/docs/audit/2026-11-09/README.md b/docs/audit/2026-11-09/README.md new file mode 100644 index 0000000..23539e4 --- /dev/null +++ b/docs/audit/2026-11-09/README.md @@ -0,0 +1,71 @@ +# Codebase audit — 2026-11-09 + +Five-minute read. Goal of the audit: make the repo parseable by humans and small-context models — each directory readable alone, no dead code, no doc that lies. Four auditors covered FundingCAPTCHA, the rest of ShowControl + IIVision, Firmware/Prototypes/PlayingThePipes, and docs. Every claim below has file:line evidence in [`detail/`](detail/). Actionable items with IDs are in [`backlog.md`](backlog.md). + +Baseline at HEAD `56de83c`: 207 commits since 2026-01-24; `make test` = 303 pytest pass, 192 skip (no GL), `node --test` finds 0 tests. + +## 1. The shape of the problem + +| | Tracked lines | Of repo | Verdict | +|---|---|---|---| +| `ShowControl/PlayingThePipes/Pipes Prototype 3/` | 133,349 | 65% | Dead. Nothing live references it. 110 files, 6.8 MB, 112 MB LFS. | +| `ShowControl/PlayingThePipes/MakeNoiseDay/` | 16,557 | 8% | Vendored personal synth library; the show patch uses 2 of 26 files. | +| `docs/superpowers/` + `docs/PRD.md` + `docs/games/` + templates | ~5,300 | 2.6% | Planning transcripts and pre-festival snapshots. All features shipped. | +| `Prototypes/` | 1,942 | 1% | Zero symbols shared with production. | +| Three "stopgap" Pico firmwares + tracked `.vscode` cache | ~870 | 0.4% | Superseded 1:1 by ESP32 targets; 483 lines of another machine's home dir. | +| Dead/duplicated Python (both ShowControl audits, conservative) | ~2,200 | 1% | Unloaded game, dead camera class, 5 copies of a GL test rig, 4 loaders of `network.json`, 2 WebSocket servers. | +| **Removable with no design decision** | **~160,000** | **78%** | | + +What remains after that is ~45k lines: ~13.6k production Python, ~6.5k test Python, 3.3k GLSL, 3.4k live firmware, ~12k lines of the one live Max project, ~3k docs. + +## 2. Three live bugs and two broken contracts + +1. **FundingCAPTCHA restart wipes per-host config.** `app.py:551` writes `{}` to `captcha-settings.local.json` on every `R` key or `/api/restart`. The file is documented as the per-host override and is read at boot; this is the only writer. Leftover from deleted corner-calibration. +2. **Two Dashboard→TreeHouse buttons 500 or no-op.** `TreeHouse/visualizer.py:112` calls a `Coordinator.set_forge_mode` that does not exist. `Dashboard/treehouse.html:45-47` sends `full`/`off`; the enum is `active`/`dim`/`inactive`. +3. **TreeHouse Pico config is unread.** `coordinator.py:177` reads key `pico`; `settings.json` has `pico_dioramas`/`pico_structure`. Driver falls back to `/dev/ttyACM0`; two pins collide. Masked because the ESP32 controllers now light the house standalone. +4. **AGENTS.md constraint #3 is violated by the repo's own firmware.** FlowerBeds `.ino:25-27` hardcodes MAC and `192.168.1.51`; `firmware_config_gen.py:20` has FlowerBeds as a TODO. Three docs say the generated `config.h` exists. +5. **5 of 11 OSC Fabric addresses were never implemented.** Only TreeHouse has an OSC listener. `/flowerbeds/mode`, `/captcha/mode`, `/pipes/mode`, `/captcha/restart`, `/captcha/game_started` are documented in four places and relayed by the Dashboard into nothing. + +Also: AGENTS.md constraint #1 names `Blob3D.world_pos_cm()`, which does not exist (it is `transforms.orbbec_to_world()`); AGENTS.md says `make test` runs Node tests (none exist since May). + +## 3. Why small models struggle here today + +- **No packaging.** No `pyproject.toml`; Elements reach `IIVision`/`OSCFabric` via `sys.path.insert`. Tests pass only because of pytest's cwd behaviour. A reader cannot tell what any package depends on. +- **Three products in one folder.** `FundingCAPTCHA/` holds the kiosk, the shader player, and the level editor, sharing `app.py` as an accidental library (`shader_app.py` imports a private `_camera_thread` from it). +- **Big files holding whole state machines as locals.** `app.py` 756 (350-line `main()`), `bodycaptcha_editor.py` 847, `body_grid_tester.py` 626 (160 lines copied from `app.py`), `coordinator.py` 485, `main.py` 454, `visualizer.py` 434 (240 of which are an HTML string), `blob_tracker.py` 434. +- **Tests that reimplement the thing under test.** 2,804 lines of shader behaviour tests copy the GL rig five times, probe private buffer layouts, and re-derive GLSL curves in Python, including an `eval()`-based GLSL const reader. Good idea, unsustainable form. +- **Docs that contradict code and each other.** 70 doc files, 9,353 lines. The OSC table is in 8 places; `network.json` is pasted verbatim in 3, all stale. `docs/agents/domain.md` and `triage-labels.md` are unmodified third-party templates. Two ADRs are numbered 0021. The glossary forbids the class names the code uses (`*Display`) because a rename stalled in May. +- **Operators are sent to files that never existed.** `PlayingThePipes.maxpat` is named in three docs; the show patch is `Lamplight2026/patchers/Lamplight2026.maxpat`. "Max/RNBO" appears in four docs; no patch contains `rnbo~`. + +## 4. The plan, in four phases + +**Phase 0 — Delete (1–2 days, no decisions).** Tag and delete Pipes Prototype 3; collapse MakeNoiseDay into Lamplight2026; delete `docs/superpowers/`, `docs/PRD.md`, `docs/games/`, the two template docs; delete `Prototypes/` (move 2 fabrication READMEs); delete 3 stopgap Picos, dead IIVision RGB camera, unloaded Keepaway game, stub screensaver, `looking_glass/deploy/`, `smoke_test.py`; untrack `.vscode` cache; drop `node --test`. Fix the three bugs and the two AGENTS.md falsehoods in the same pass. Result: 78% fewer lines, every surviving directory is live. + +**Phase 1 — Make packages real (1 day).** Root `pyproject.toml` with `IIVision` and `OSCFabric` installable and per-Element extras; delete `sys.path` hacks and the four `requirements.txt`; TreeHouse and Dashboard import `OSCFabric.load_network_config` instead of their own. Finish `firmware_config_gen.py` so constraint #3 is true. Per-directory README stubs (≤20 lines) for `IIVision/`, `OSCFabric/`, `games/`, `Firmware/`, `scripts/`, `PlayingThePipes/`. + +**Phase 2 — Split by concern (4–6 days, mostly mechanical).** FundingCAPTCHA: extract `settings.py`/`camera_feed.py`/`monitor.py` from `app.py`; collapse `shader_app` into `playlist_app --setup`; shared `shader_test_rig.py` and public `Renderer.read_buffer()`, tests move into `shaders//`. Core: `ShowWeb.LiveServer` replaces two identical WebSocket servers; `blob_tracker.py` → `types/calibration/detector`; `FlowerBeds/main.py` → `controllers.py` + `calibrate_motors.py`; `coordinator.py` → `config/garden_state/branches`. Every file lands under ~300 LOC. + +**Phase 3 — Docs to one job each (1–2 days, after code settles).** 70 files → ~30, 9.3k lines → ~3k. New `docs/repo-map.md` and `docs/osc-fabric.md` as the single homes for layout and addresses; `docs/agents/` shrinks to `conventions`, `firmware`, `git-workflow`; ADRs get a status line, truncated superseded bodies, a README index, and the 0021 collision renumbered. `CONTEXT.md` becomes glossary-only and gains the five location names it currently omits. + +## 5. Decisions (made 2026-11-09) + +| # | Decision | Outcome | Effect | +|---|---|---|---| +| D1 | ESP32-S3 controllers installed? | **Yes** | Execute ADR-0020's removal of the Pi Pico LED path (P0-14, P0-18, P2-13 ungated). | +| D2 | Windows PyInstaller editor build still used? | **No** | Delete `distribution/` (P0-11). | +| D3 | Five unimplemented OSC addresses | **Remove from schema** | Strike from ADR-0007 and docs; delete Dashboard `/api/mode` relay (P0-16). | +| D4 | 20 unreferenced Lamplight2026 media files | **Keep all** | P0-03 reduces to deleting the byte-identical Prototype 3 copies, which P0-01 already does. | +| D5 | Keep `body_keepaway.py`? | **Delete** | P0-09 ungated. | +| D6 | Where do Pipes samples live? | **Git LFS here** | No change. | + +Nothing in Phase 0 is gated any more. + +## 6. Where to drill down + +- [`backlog.md`](backlog.md) — 53 tasks with IDs, effort, files, done-when, and source section. Filed as GitHub issues #175–#197 (Phase 0 items individually, Phases 1–3 as umbrellas). +- [`detail/fundingcaptcha.md`](detail/fundingcaptcha.md) — 487 lines: inventory, dead code, 13 duplication sites, module splits, config key→reader table, test critique. +- [`detail/showcontrol-core.md`](detail/showcontrol-core.md) — 780 lines: IIVision/FlowerBeds/TreeHouse/Dashboard/OSCFabric/scripts, packaging plan, settings keys nothing reads, ADR-by-ADR drift. +- [`detail/firmware-prototypes-pipes.md`](detail/firmware-prototypes-pipes.md) — 253 lines: live/dead per firmware dir, Max patch reachability analysis, media sizes, LFS. +- [`detail/docs.md`](detail/docs.md) — 466 lines: every doc with verdict, every stale claim doc:line → code, redundancy map, target doc tree, README stub contents. + +This directory is itself temporary. The backlog is now in the issue tracker (#175, #176, #178, #179); delete `docs/audit/` when Phase 3 closes (P3-09). diff --git a/docs/audit/2026-11-09/backlog.md b/docs/audit/2026-11-09/backlog.md new file mode 100644 index 0000000..f89fd77 --- /dev/null +++ b/docs/audit/2026-11-09/backlog.md @@ -0,0 +1,325 @@ +# Audit backlog — 2026-11-09 + +Each item is sized to be one PR and one issue. Effort: **S** ≤ half day, **M** ≈ 1 day, **L** 2–3 days. All six decisions in [README §5](README.md#5-decisions-made-2026-11-09) were made on 2026-11-09; the "Gate" column records which decision each item depended on. Nothing is gated now. "Source" points at the section of the detail report with the evidence. Paths are repo-relative; `FC/` = `ShowControl/FundingCAPTCHA/`, `TH/` = `ShowControl/TreeHouse/`, `FB/` = `ShowControl/FlowerBeds/`. + +Run `make test` before and after every item; it must stay at 303 passed. Items that delete tests say so. + +--- + +## Phase 0 — Delete and fix (no design decisions unless gated) + +| ID | Task | Effort | Gate | Source | +|---|---|---|---|---| +| P0-01 (#180) | Tag and delete `Pipes Prototype 3/` | S | — | firmware §3b, §3c | +| P0-02 (#181) | Collapse `MakeNoiseDay/` into `Lamplight2026/` | S | — | firmware §3b | +| P0-03 (no-op) | Prune `Lamplight2026/media/` | S | D4 ✓, D6 ✓ | firmware §3d | +| P0-04 (#182) | Delete `docs/superpowers/`, `docs/PRD.md`, `docs/games/`, two template docs | S | — | docs §1, §5, §6 | +| P0-05 (#183) | Delete `Prototypes/`; move 2 fabrication READMEs | S | — | firmware §2 | +| P0-06 (#184) | Delete `Club_Pico`, `HouseSwarming_Pico`, `Fungi_Pico` | S | — | firmware §1a | +| P0-07 (#185) | Untrack `.vscode` cache; add PlatformIO ignores | S | — | firmware §1e, §5 | +| P0-08 (#186) | Fix `_save_local({})` bug | S | — | fundingcaptcha §2 | +| P0-09 (#187) | Delete unloaded Keepaway game and its data | S | D5 ✓ | fundingcaptcha §2 | +| P0-10 (#188) | Delete FundingCAPTCHA dead members | S | — | fundingcaptcha §2 | +| P0-11 (#189) | Delete `FC/distribution/` and the `sys.frozen` branch | S | D2 ✓ | fundingcaptcha §2 | +| P0-12 (#190) | Delete dead IIVision RGB camera + unused exports | S | — | core §2 | +| P0-13 (#191) | Delete core dead code (`looking_glass/deploy/`, `smoke_test.py`, setters, `ControllerConfig`…) | S | — | core §2 | +| P0-14 (#192) | Retire TreeHouse Pico LED path per ADR-0020 | M | D1 ✓ | core §2 "Live-but-inert", firmware §1b | +| P0-15 (#193) | Fix Dashboard→TreeHouse controls | S | — | core §0.2 | +| P0-16 (#194) | Remove the 5 unimplemented OSC addresses | S | D3 ✓ | core §0.3, docs §2 ADR-0007 | +| P0-17 (#195) | Fix AGENTS.md: constraint #1 function name, constraint #3 claim, Node tests; drop `node --test` from Makefile | S | — | docs §2 AGENTS.md | +| P0-18 (#196) | Fix TreeHouse `settings.json` unread keys | S | D1 ✓ | core §6 | +| P0-19 (#197) | Delete 16 unreferenced LFS images | S | — | fundingcaptcha §1 | + +### P0-01 Tag and delete `Pipes Prototype 3/` +- **Why:** 110 files, 133,349 lines, 6.8 MB tracked (64% of repo bytes), 112 MB LFS. Transitive reachability from `Lamplight2026.maxpat` over bpatcher names and object text reaches nothing in it. Ten `.amxd`/`.maxpat` pairs are byte-identical apart from a 20-byte header. Its 24 media files are byte-identical to Lamplight2026's. +- **Do:** `git tag archive/pipes-prototype-3 56de83c && git push origin archive/pipes-prototype-3`; `git rm -r "ShowControl/PlayingThePipes/Pipes Prototype 3"`. +- **Done when:** directory gone; tag on remote; `docs/PlayingThePipes.md` does not mention it. + +### P0-02 Collapse `MakeNoiseDay/` into `Lamplight2026/` +- **Why:** Vendored personal Max library (own MIT LICENCE + README with streaming links). Lamplight2026 uses exactly two files via `knobgrain.maxpat`: `scaleclip.maxpat`, `wakemove.maxpat`. `Lamplight2026.maxproj:20-40` pins three MakeNoiseDay deps by `~/Projects/...` absolute bootpath; one (`autograin`) is referenced by nothing. +- **Do:** copy the two patchers into `Lamplight2026/patchers/`; remove the three `singleton.bootpath` entries from the `.maxproj`; delete `MakeNoiseDay/` (24 files, 16,178 lines). Open the project in Max once to confirm it resolves. +- **Done when:** `Lamplight2026.maxproj` has no path outside its own folder; `MakeNoiseDay/` gone. + +### P0-03 Prune `Lamplight2026/media/` — D4: keep all; D6: LFS stays +- **Why:** 33 LFS files, 114 MB. 20 files (41 MB, incl. `tone.04.wav` 22 MB) are named by no patcher. Max can load buffers by message at runtime, so static grep is necessary but not sufficient. +- **Decided:** keep all Lamplight2026 media in LFS. Only the Prototype 3 duplicates go (P0-01). `grainflow.wav` is referenced but absent from `media/`; confirm it is a Grainflow package sample. +- **Done when:** P0-01 is done; nothing further. + +### P0-04 Delete planning artefacts and template docs +- **Why:** `docs/superpowers/` is 13 files / 4,328 lines (46% of all doc lines) of AI pair-programming transcripts; all five features are shipped and tested. `docs/PRD.md` is a May-2026 pre-festival snapshot. `docs/games/rhythm.md`, `upsidedown.md` describe code deleted in May; `bodycaptcha.md`/`bodygrid.md`/`keepaway.md` are stale (hold "resets" vs drains, progress bar vs ring, wrong module path, wrong `update()` signature). `docs/agents/domain.md` and `triage-labels.md` are unmodified third-party templates referencing files that do not exist here. +- **Do:** `git rm -r docs/superpowers docs/games docs/PRD.md docs/agents/domain.md docs/agents/triage-labels.md`. Salvage five one-liners into `common.glsl` headers (list in docs §5). Fix inbound links: `docs/FundingCAPTCHA.md:61`, `docs/adr/0022…:1448`. Put the Rhythm/UpsideDown port in one GitHub issue if still wanted. Fold the 5-line label table into `git-workflow.md`. +- **Done when:** `grep -r superpowers docs` is empty; `make test` unchanged. + +### P0-05 Delete `Prototypes/` +- **Why:** 14 TouchTest symbols and 3 ForgeAndFlora symbols have zero hits in `ShowControl/`+`IIVision/`. `TH/test_forge_and_flora.py` tests production `displays/effect.py`, not the prototype. GlowPanels' two build READMEs are the fabrication record of the shipped Mycelium panel (`NUM_LEDS=224` matches settings). +- **Do:** move `Prototypes/GlowPanels/01_SideLit/README.md` and `02_BackLit_Frosted/README.md` to `docs/fabrication/`; `git rm -r Prototypes`. +- **Done when:** no `Prototypes/` dir; architecture doc no longer needs to explain it. + +### P0-06 Delete the three stopgap Pico firmwares +- **Why:** Each opens with "standalone Pico light controller (stopgap)", one-day commits 2026-05-29/30, zero references anywhere, each duplicates the 15-line PIO driver from `TreeHouse_PicoLEDs`. Superseded 1:1 by `TreeHouse_Controllers/src/targets/{swannatopia,jess,julia,dormer}.h`. +- **Do:** `git rm -r Firmware/Club_Pico Firmware/HouseSwarming_Pico Firmware/Fungi_Pico` (283 LOC). +- **Done when:** gone; `Firmware/README.md` (P1-05) lists them as retired. + +### P0-07 Untrack `.vscode` cache, add PlatformIO ignores +- **Why:** `Firmware/TreeHouse_Controllers/.vscode/c_cpp_properties.json` (538 lines, 483 contain another machine's home path) and `launch.json` are tracked despite being in that project's own `.gitignore`. +- **Do:** `git rm --cached` both; add `**/.pio/`, `**/.vscode/c_cpp_properties.json`, `**/.vscode/launch.json`, `**/.vscode/ipch` to root `.gitignore`. Also delete `.gitignore:17` `layout_calibrated.json` (file removed by ADR-0015) and fix the stale `touch_test_pygame.py` comment at `:18`. +- **Done when:** `git ls-files -i -c --exclude-standard` is empty. + +### P0-08 Fix `_save_local({})` +- **Why:** `FC/app.py:551` in `_do_restart()` writes `{}` to `captcha-settings.local.json`. That file is the per-host override (`load_settings`, `app.py:82-84`); this is the only writer. Any override is wiped on the first `R` key or `/api/restart`. Corner-cal leftover from ADR-0012, never removed by ADR-0013. +- **Do:** delete `_save_local` and the call. Add a restart test (none exists) that asserts the file is untouched. Fix `docs/FundingCAPTCHA.md:93` ("not gitignored" — it is, `.gitignore:19`). +- **Done when:** restart leaves the local file byte-identical; test proves it. + +### P0-09 Delete Keepaway — D5: delete +- **Why:** `FC/games/body_keepaway.py` (498 LOC), `keepaway-body-levels.json`, `keepaway-body-taunts.json`, `captcha-settings.json:44-49`. `app.py:349-351` hardcodes `("bodycaptcha",)`; `test_app_games.py:26-28` asserts Keepaway is not loaded. Unplugged in #142 (Sept), untouched since June. Git history is the retention mechanism. +- **Do:** `git rm` the module and two JSON files; remove the settings block; drop the two assertions in `test_app_games.py`; delete `docs/games/keepaway.md` (P0-04 covers it). Also remove `BLUE` import and `hud_font`/`top_bar_height` imports left in `games/`. +- **Done when:** `grep -ri keepaway` returns only ADR-0013's historical note. + +### P0-10 Delete FundingCAPTCHA dead members +- **Why/what:** `ScreenSavers/waiting.py` (stub; `pipes_3d.py:382-402` draws the same overlay) + its `screensavers.json` entry; `games/grid.py:18-24` 7 colour aliases, `:26,90-91` `CELL_PAD`/`inner_rect`, `:67,93-115` `_flashes`/`flash`/`draw_flashes`, `:97-102` `draw_background`, `:117-120` `blit_cell`; `app.py:69-73` unused colours, `:284-285` empty section header; `app.py` `/ws` status channel + `broadcast()` (`:104,133-163,189-194`) — no client connects (Dashboard uses `/health` + `/logs`); `body_blobs.py:127-129` `BodyBlobs.active`; `captcha-settings.json:6` `detection.min_blob_pixels` (never consulted by `detect_foreground`); stale comments in `deploy/captcha.service:1,6-7`. +- **Done when:** ~150 LOC gone; `make test` unchanged. + +### P0-11 Delete `FC/distribution/` — D2: delete +- **Why:** 297 LOC + READMEs. All three scripts from one commit (#94, 2026-05-23), never edited; the editor changed three times in Sept with no rebuild trail; `BodyCaptchaEditor.command` (#152) is the newer teammate path and the README itself tells Mac users to commit on a branch instead. `merge_levels.py` has no tests. `bodycaptcha_editor.py:16-19` `sys.frozen` branch exists only for this. +- **Done when:** directory and frozen branch gone; `docs/FundingCAPTCHA.md:128` updated. + +### P0-12 Delete dead IIVision RGB camera +- **Why:** `IIVision/camera.py:38-53, 303-425` `OrbbecRGBCamera`/`RGBFrame` existed only for the ArUco calibrator deleted by ADR-0015. Zero importers. Needs `cv2`, which no requirements file lists. Also exported from `__init__.py:18,34-35`. +- **Do:** delete the classes, the `cv2` import, the section headers, the `__init__` exports; drop unused imports `camera.py:19,23,91`; drop `Blob3D.cam_half_extents_m/median_z_m/sample_count` (`blob_tracker.py:81-83,429-431`, computed never read), `Calibration.delete()` (`:138-141`), `CalibrationConfig` (`:47-49`), `TrackState`/`PreStabilizeFilter` exports. +- **Done when:** ~150 LOC gone; IIVision tests pass. + +### P0-13 Delete core dead code +- **What:** `TH/looking_glass/deploy/` (service self-declared OBSOLETE by ADR-0018; its `install.sh` is the only one adding the user to `_seat` — see P1-06); `scripts/hooks/smoke_test.py` (no-op stub, wired nowhere); `FB/flower_beds.py:128-135` `ControllerConfig` (unused, hardcodes `192.168.1.50`); `FB/visualizer.py:377-382` `_get_loop`; `FB/main.py:222-223,274` `calibration_state` (constant); unused setters `TH/displays/effect.py:51-62`, `video.py:57-61`, `led.py:49-53`, `base.py:58-62`, `coordinator.py:412-413` `Coordinator.get`; `FB/requirements.txt` `aiofiles`; `TH/requirements.txt` `numpy`; all pyflakes unused imports (list in core §2). +- **Done when:** ~110 LOC gone; pyflakes clean on `IIVision ShowControl/FlowerBeds ShowControl/TreeHouse ShowControl/Dashboard ShowControl/OSCFabric`. + +### P0-14 Retire TreeHouse Pico LED path — D1: ESP32s installed, retire +- **Why:** ADR-0020 promised removal once ESP32 hardware is installed. ADR-0021 made the ESP32s standalone. The Pi path cannot drive the documented hardware anyway: `coordinator.py:177` reads `pico`, settings has `pico_dioramas`/`pico_structure`; single `PicoDriver` at `main.py:149`; pin collisions (`dormer.pico_pin=0` vs `dioramas[0]`, `porch_lights=1` vs `dioramas[1]`); `LEDDisplay.update()` ignores GardenState (`led.py:60-63`). +- **Do:** delete `pico_driver.py`, `displays/led.py`, `displays/effect.py`, `displays/porch_lights.py`, `LEDControllable`/`ChannelFrame`/`Color` in `base.py`, LED branches of `coordinator.py` (`DioramaConfig`, `DormerConfig`, F&F/porch config, `build_displays` LED cases, `get_all_frames`, `set_display_pattern`, `reset_porch_lights`), `test_forge_and_flora.py`, Dashboard LED/forge/porch controls (`treehouse.html` ~60 lines, `visualizer.py` endpoints `:92-127`), `Firmware/TreeHouse_PicoLEDs/`, `pico_*` + `attic` + `garage_windows` settings blocks, `--no-pico` flag, `docs/TreeHouse.md:15-17,164-171,234`. Finish the `*Display` → `*Controllable` rename at the same time (4 files). Mark ADR-0010 Superseded. +- **Done when:** `TH/` has one lighting path and the glossary's avoid-list matches the class names. ~800 LOC. + +### P0-15 Fix Dashboard→TreeHouse controls +- **Why:** `TH/visualizer.py:112` calls nonexistent `Coordinator.set_forge_mode` → 500. `Dashboard/treehouse.html:45-47` sends `full`/`dim`/`off`; `displays/base.py:10-13` is `active`/`dim`/`inactive`. `Dashboard/serve.py` `POST /api/mode` is called by no page. +- **Do:** change button values; delete `/control/forge-mode` + its button (the whole path is a no-op since `effect.py:79` overwrites mode every frame). Decide whether `serve.py /api/mode` stays (ties to P0-16). Add a FastAPI TestClient test for each `/control/*` route; there are none. +- **Done when:** every Dashboard button does what its label says or does not exist. + +### P0-16 Remove the 5 unimplemented OSC addresses — D3: remove from schema +- **Why:** ADR-0007 lists 11 addresses. `/flowerbeds/mode`, `/captcha/mode`, `/pipes/mode`, `/captcha/restart`, `/captcha/game_started` have no sender or receiver (only `OSCFabric/test_client.py:107` sends `game_started`). Only TreeHouse runs an OSC server. Dashboard relays mode into nothing for 3 of 4 Elements. Documented as live in `docs/FlowerBeds.md:103`, `docs/FundingCAPTCHA.md:158`, `docs/Dashboard.md:15,28`, ADR-0007:159-162. +- **Do:** strike the five rows from ADR-0007 and the three docs; delete `serve.py /api/mode` and `send_event("game_started")` test. +- **Done when:** every address in the schema has a grep-able sender and receiver; the Dashboard has no relay endpoint nobody calls. + +### P0-17 Fix AGENTS.md and Makefile +- **What:** `AGENTS.md:13` `Blob3D.world_pos_cm()` → `IIVision.transforms.orbbec_to_world()` (`world_pos_cm` is a *field* on `TrackedBlob`). `AGENTS.md:15` "firmware reads a generated `config.h`" → true only for `TreeHouse_Controllers/include/net_config.h`; either say so or close P1-04 first. `AGENTS.md:8` "Node tests (FundingCAPTCHA)" → none exist since 2026-05-11; drop `node --test` from `Makefile`, `.github/workflows/tests.yml` setup-node step, and `requirements`. Same fixes in `docs/agents/python-conventions.md:20,28`, `running-and-testing.md:6`, `firmware.md:9`, `configuration.md:113`. +- **Done when:** every sentence in AGENTS.md is grep-verifiable. + +### P0-18 Fix TreeHouse `settings.json` unread keys — mostly subsumed by P0-14 +- **What:** `pico_dioramas`/`pico_structure` (code reads `pico`), `garage_windows.forge_and_flora.arc_led_count`/`bloom_led_count` (code reads `led_count`, default 48 on 20-px strips), `flicker_intensity` (code reads `base_`/`max_flicker_intensity`), whole `attic` block (nothing reads `attic`), `club_screen.interval_seconds` (club_screen reads six other keys), `looking_glass.mirror_depth` (read, never sent). ~25 lines. +- **Do:** delete or rename each so every key has a reader; `club_screen.py` should also honour `--config` (it always loads `./settings.json`) and stop hardcoding 800×480. +- **Done when:** a script that diffs settings keys vs `raw.get(...)` calls reports zero orphans. + +### P0-19 Delete 16 unreferenced LFS images +- **Why:** `FC/images/yeavl_may/` holds 24 files; `bodycaptcha-levels.json` references 8 of them plus `images/jess.jpg`. The 16 are LFS objects pulled on every Pi install (`deploy/install.sh:30`). +- **Done when:** every tracked image is referenced by a level file. + +--- + +## Phase 1 — Make packages and directories self-describing + +| ID | Task | Effort | Gate | Source | +|---|---|---|---|---| +| P1-01 | Root `pyproject.toml`; delete `sys.path` hacks and per-Element `requirements.txt` | S | — | core §5 | +| P1-02 | One `load_network_config` | S | — | core §3b | +| P1-03 | Fix PlayingThePipes docs; add `PlayingThePipes/README.md` | S | — | firmware §3a, §6 | +| P1-04 | Finish `firmware_config_gen.py` for FlowerBeds | S | — | firmware §1d | +| P1-05 | README stubs: `IIVision/`, `OSCFabric/`, `FC/games/`, `Firmware/`, `scripts/` + 3 live firmware dirs | S | — | docs §7, firmware §1f | +| P1-06 | Shared `scripts/lib/install-element.sh`; fix `_seat` gap | S | — | core §3g, §8 | +| P1-07 | Fold `ESP32S3_SignsOfLife` into `TreeHouse_Controllers` as an env | S | — | firmware §1c | +| P1-08 | Run `pio test -e native` in CI | S | — | firmware §1e | +| P1-09 | Move `TouchOSC/` beside `FB/diag.py` or delete | S | — | firmware §4 | +| P1-10 | `requirements-dev.txt`: add `av`, `Pillow`, `pyserial` so no test silently skips | S | — | fundingcaptcha §7 CI | + +### P1-01 Root `pyproject.toml` +- **Why:** No `pyproject`/`setup.py`/`conftest.py`. `FB/main.py:32,53`, `FC/app.py:42-44`, `body_blobs.py:28`, `body_grid_tester.py:47-48` do `sys.path.insert`. Tests pass only because `python -m pytest` puts cwd on `sys.path`. Four `requirements.txt` are partial, inconsistent copies. +- **Do:** the `pyproject.toml` in core §5 "Minimal change" (packages `IIVision`, `OSCFabric`; extras `flowerbeds`, `treehouse`, `dashboard`, `fundingcaptcha`, `dev`; `[tool.pytest.ini_options] testpaths`, `pythonpath=["."]`). `deploy/install.sh` → `pip install -e "$REPO_ROOT[]"`. Delete `pytest.ini`, `requirements*.txt`, the `sys.path` lines. CI installs `.[dev,flowerbeds,treehouse,dashboard,fundingcaptcha]`. +- **Done when:** `pytest` from any cwd passes; `python -c "import IIVision, OSCFabric"` works after `pip install -e .`. + +### P1-02 One `load_network_config` +- **Why:** Four implementations: `OSCFabric/config.py:13-20`, `TH/coordinator.py:169-175` (derives path from `--config`, so a non-default settings path silently loses network.json), `Dashboard/serve.py:34-37`, `FB/layout_tool.py:50-52`; plus the try/except block copied 3× in `FB/main.py:124-128,301-305,358-362`. +- **Do:** everyone imports `OSCFabric.load_network_config`; add `load_network_config_or_empty(log)`. `Dashboard/index.html:55-60` hardcodes the four `http_port`s that `network.json` already holds — serve them from `/api/network` or template them. +- **Done when:** `grep -rn "network.json" --include=*.py` shows one path resolution. + +### P1-03 PlayingThePipes docs and README +- **Why:** `docs/PlayingThePipes.md:11,43,93`, `docs/bootstrap.md:173,198` name `PlayingThePipes.maxpat`, which has never existed on `main` (only on unmerged branch `claude/max-music-system-stub-z2Qs0`). Show patch is `Lamplight2026/patchers/Lamplight2026.maxpat`. "Max/RNBO" in 4 docs; no `rnbo~` anywhere. Engine externals (`grainflow~`, `abl.device.*`) are undocumented. COM port table "TBD" since May; `encoderin.maxpat` takes `serial #1`/`#2` as patcher args. +- **Do:** replace every path; drop RNBO; list externals and the two serial args; add `ShowControl/PlayingThePipes/README.md` naming the show project, `EncoderTester.maxpat`, `health_server.py`. +- **Done when:** an operator following `bootstrap.md` opens a file that exists. + +### P1-04 Finish `firmware_config_gen.py` for FlowerBeds +- **Why:** `Firmware/FlowerBeds_Follow_ServoController/*.ino:25-27` hardcodes MAC and `192.168.1.51`; `scripts/hooks/firmware_config_gen.py:20` is a TODO. AGENTS.md constraint #3 and three docs claim otherwise. `firmware.md:10` already warns about MAC/IP collision on the second board. +- **Do:** render `config.h` for `flowerbeds_controller_1/2` (~20 lines Python); `#include "config.h"` in the sketch; gitignore or commit-by-design like `net_config.h`. Note `network.json` `firmware.*.mac` is read by nothing today. +- **Done when:** the `.ino` has no literal IP or MAC. + +### P1-05 README stubs +- **Why:** Only 2 of 9 firmware dirs and 0 of the Python packages have one. A model opening `IIVision/` has no map. Contents per stub are specified in docs §7 "Per-directory README stubs" (≤20 lines each). +- **Do:** `IIVision/README.md`, `ShowControl/OSCFabric/README.md`, `FC/games/README.md`, `Firmware/README.md` (table: dir → board → drives → live/retired), `scripts/README.md`, and 10-line READMEs for `FlowerBeds_Follow_ServoController`, `TreeHouse_BranchController`, `PlayingThePipes_EncoderPico` (board, libs, protocol, flash). +- **Done when:** every top-level and firmware directory has a README that says what it is in ≤20 lines. + +### P1-06 Shared installer; fix `_seat` +- **Why:** `FB/deploy/install.sh:30-62`, `TH/deploy/install.sh:8-52`, `Dashboard/deploy/install.sh:26-56` share SCRIPT_DIR/REPO_ROOT, `SERVICE_USER`, `pip3 install --break-system-packages`, the `sed User=` into `/etc/systemd/system`, `daemon-reload/enable/restart`, the echo trailer. `treehouse.service:16` sets `LIBSEAT_BACKEND=seatd` (needs group `_seat`) but the live `TH/deploy/install.sh` never adds it; only the obsolete `looking_glass/deploy/install.sh:20` does. Hardcoded `/home/ii/CommunityGarden` in all four `.service` files and `deploy.sh:76,78`. +- **Do:** `scripts/lib/install-element.sh` with `install_element [apt...]`; each `deploy/install.sh` ≈ 8 lines; template `WorkingDirectory=` from `REPO_ROOT`; add `usermod -aG _seat` to TreeHouse. Drop stale comments in `flowerbeds.service:6-7`, `treehouse.service:6-7,22`. +- **Done when:** a fresh Pi with a non-default clone path installs and starts TreeHouse. + +### P1-07 Fold `ESP32S3_SignsOfLife` into `TreeHouse_Controllers` +- **Why:** Its `platformio.ini:16-29` duplicates `TreeHouse_Controllers/platformio.ini:20-32` verbatim; second `.vscode/` and `.gitignore`. The README's boot-loop triage section is genuinely useful. +- **Do:** `[env:signsoflife]` with `build_src_filter`; move triage README into a "Bring-up" section of the main README; delete the directory. + +### P1-08 Firmware tests in CI +- **Why:** 51 Unity tests across 4 suites (`pio test -e native`) exist and pass but nothing runs them; CI is Node + Python only. +- **Do:** add a `pio test -e native` job to `tests.yml` (or a Makefile target and a line in `firmware.md` saying it is manual). + +### P1-09 `TouchOSC/` +- **Why:** A top-level directory for one 4 KB binary (4 faders → `/cg/ff/rot`). `FB/diag.py` does the same job from Python. Referenced by `running-and-testing.md:69`, `architecture.md:19`. +- **Do:** move to `ShowControl/FlowerBeds/tools/FlowerBedTester.tosc` with a 5-line README, or delete and fix the two refs. + +### P1-10 Dev requirements +- **Why:** `requirements-dev.txt` lacks `av` (so `test_playlist_app` video tests `importorskip` → silently skipped in CI), `Pillow`, `pyserial` (no test imports `TH/main.py` *yet*). Superseded by P1-01's `dev` extra if done together. + +--- + +## Phase 2 — Split by concern + +| ID | Task | Effort | Gate | Source | +|---|---|---|---|---| +| P2-01 | Extract `FC/settings.py`, `camera_feed.py`, `monitor.py` from `app.py`; tester and shader stage import them | M | — | fundingcaptcha §3a–e, §4 | +| P2-02 | Collapse `shader_app` into `playlist_app --setup`; one Setup list | S | — | fundingcaptcha §3j–k, §4 | +| P2-03 | `shader_test_rig.py` + public `Renderer.read_buffer()`; tests move into `shaders//` | M | — | fundingcaptcha §3l, §4 tests | +| P2-04 | Prune layout-probing and half-float shader tests to spec-level behaviour | M | — | fundingcaptcha §4 tests, §7 | +| P2-05 | Split `app.main()` into `Kiosk` with per-state ticks; first state-machine tests | L | — | fundingcaptcha §4, §7 | +| P2-06 | Split `bodycaptcha_editor.py` into model/widgets/views; shared `levels.py` with the game | L | — | fundingcaptcha §3i, §4 | +| P2-07 | `games/grid.py` → `grid.py` + `shell.py`; `blowup.py`; tests for `Grid` and `_LevelBag` | S | — | fundingcaptcha §4, §7 | +| P2-08 | `body_grid_tester.py`: fold into `app.py --tester` or rebuild on P2-01 modules | S | — | fundingcaptcha §4 | +| P2-09 | `ShowWeb.LiveServer`; FlowerBeds HTML string → static file | M | — | core §3a, §4 | +| P2-10 | `OSCFabric/gate.py` `SignalGate`; `FabricClient` and `LocationSender` wrap it | S | — | core §3d | +| P2-11 | `FB/controllers.py` + `calibrate_motors.py` + `state.py`; `main.py` → ~180 LOC | M | — | core §3c, §4 | +| P2-12 | `IIVision/blob_tracker.py` → `types/calibration/detector`; rename `BlobTracker`→`BlobDetector`; add detect/calibrate tests | M | — | core §4 | +| P2-13 | `TH/coordinator.py` → `config.py` + `garden_state.py` + `branches.py`; `BranchController` as a `Controllable` | M | D1 ✓ first | core §4 | +| P2-14 | `shadertoy.py` → package (`setup/body/keyboard/blobs_contract/renderer`) | S | — | fundingcaptcha §4 | +| P2-15 | Settings `DEFAULTS` + startup validation for `captcha-settings.json` | S | after P2-01 | fundingcaptcha §5 | + +### P2-01 Extract shared FundingCAPTCHA infrastructure +- **Why:** `app.py:218-281` camera thread ≈ `body_grid_tester.py:190-253`; `app.py:104-213` monitoring server ≈ `tester:105-185`; `app.py:80-87` `load_settings` byte-identical to `tester:75-82`; camera factory ×3 (`app:486-494`, `tester:489-497`, `shader_app:197-202`); slab/ROI defaults ×7 with values that disagree with the committed file (code 800–2500 mm / 640×400; file 1000–4000 / 480×480); slab-mask fold ×4; colour tables ×4. `shader_app.py` imports private `_camera_thread` from `app`, dragging pygame display code along for a 7-line function. +- **Do:** `settings.py` (`DIR`, `load_settings`, `DEFAULT_SLAB`, `default_roi`, `slab_color`, `slab_mask`), `camera_feed.py` (`CameraFeed.start/restart/stop`, `.queue`, standback gate, `make_camera`), `monitor.py` (`MonitoringServer(port, commands)`), `test_input.py`, `plugins.py` (`load_games`, `load_screensavers`). Tester and shader stage import these. Make code defaults equal the file. +- **Done when:** `grep -n "from app import" FC/*.py` is empty; one definition of each default. + +### P2-02 One shader entry point +- **Why:** `shader_app.Rotation` (`:86-144`) ⊂ `playlist_app.Playlist` (`:225-334`); `shader_app.main` (`:330-362`) ⊂ `playlist_app.main` (`:365-411`); `shaders/setups.json` and `playlist.json:setups` are two hand-synced lists. Only `shader_app --setup NAME` has no playlist equivalent. Tests duplicate too (`test_shadertoy.py:307-345` vs `test_playlist_app.py:128-203`). +- **Do:** add `--setup` to `playlist_app` (one-item playlist); delete `Rotation`, `shader_app.main`, `setups.json`; rename remainder `shader_stage.py` (library); split `video_player.py` out of `playlist_app.py`. Update `docs/FundingCAPTCHA.md:74`, `deploy/captcha.service` comment, `test_shadertoy.py:179`. +- **Done when:** one entry point, one Setup list. + +### P2-03 Shared shader test rig +- **Why:** `ctx()` fixture ×5 (`test_shadertoy:157-171`, `test_endless_sunset:22-36`, `test_hunter_bird:27-41`, `test_watering_can:59-73`, `test_rainbow_poop:44-58`); `Rig` class ×4; mask builders (`rect/person/walking/standing/flickering/both`) ×4; GLSL-const readers ×3 with three different mechanisms incl. an `eval()` mini-interpreter (`test_watering_can:23-50`). All four read private `Renderer._buffers[name].read_tex`. `test_playlist_app.py:11` imports fixtures from another test module. +- **Do:** `FC/shaders/shader_test_rig.py` (or `conftest.py`): `ctx`, `Rig`, masks, `glsl_consts()`, ~200 LOC. Add `Renderer.read_buffer(name)` beside `read_body()`. Move `test_.py` into `shaders//` so a deleted Setup takes its tests with it. Note the pattern in `shaders/README.md` "Hand it in". +- **Done when:** −350 LOC; no test touches `_buffers`. + +### P2-04 Prune layout and half-float tests +- **Why:** 2,804 LOC to test ~3,000 LOC of GLSL. `test_owner_is_record_slot`, `test_run_extents_span_leg_to_shoulder`, `test_walker_leaves_no_hold_trail` (asserts a quantisation encoding), `test_slot_tracks_record_id`, `test_ages_keep_time_in_half_floats` pin texel storage, not visitor-visible behaviour. `test_watering_can:271-292` parametrises on "coarsest half-float spacing"; `:598,610` assert llvmpipe f16 stalls. `grown()/seconds_to()` (`:318-329`) re-derive the GLSL growth curve in Python. Each Setup test renders hundreds of frames; `BigRig` renders 480×480 ×40 ×3. +- **Do:** keep 10–15 spec-level tests per Setup (the claims from the deleted design docs); move input-contract tests (Buffer A, blobs texture) to one `test_shader_contracts.py` owned by `shadertoy.py`/`body_blobs.py`; replace re-derived curves with monotonicity/ordering asserts or recorded fixtures; add a `--durations` budget check in CI. +- **Done when:** suite is <1,500 LOC and CI GL time is bounded. + +### P2-05 `Kiosk` class +- **Why:** `app.main()` is 350 lines (`:408-752`) holding the BG_CAL/SCREENSAVER/GAME machine in 15 loose locals and closures; the `R`-key and HTTP restart blocks are two slightly different copies (`:583-591` resets `sil_cache`, `:598-604` does not). Zero tests on the show path; P0-08's bug would have been caught by any restart test. +- **Do:** `kiosk.py` with `AppState`, `Kiosk` dataclass, `_tick_bg_cal/_tick_screensaver/_tick_game`, one `restart()`. `app.py` → argparse + wiring (~40 LOC). Add tests for transitions, standback/countdown gating (`:649-687`), attract dwell, OSC throttle, restart. +- **Done when:** `app.py` <100 LOC; state machine has tests. + +### P2-06 Editor split; shared `levels.py` +- **Why:** `bodycaptcha_editor.py` is one 700-LOC class mixing model, widgets, three screens. Levels-path override is duplicated (`games/bodycaptcha.py:30-46` vs `editor:20-32`), `_img_load` duplicated (game version has an LFS-pointer check the editor lacks), `DEFAULT_LEVEL` exists three times with three different values. `Editor.__init__` opens a display, so `test_bodycaptcha_editor.py:22-33` opens one to test string logic. `set_levels_path` is module-global; two test files need autouse restore fixtures and `test_app_games.py:45-50` exists only to detect leakage. +- **Do:** `FC/levels.py` (`read_levels`, `write_levels`, `DEFAULT_LEVEL`, `Levels` object passed into the game, `load_image`); `editor/{levels_io,widgets,level_model,views}.py`; `bodycaptcha_editor.py` → run loop + argparse. `BodyCaptchaGame` takes `(WW, WH)` instead of reading `pygame.display.get_surface()` (`bodycaptcha.py:202`) so tests stop opening displays. +- **Done when:** no file >300 LOC; no test opens a display to test non-drawing logic; no autouse restore fixtures. + +### P2-07 `grid.py` / `shell.py` / `blowup.py` +- **Why:** ADR-0019 suggested `games/shell.py`; implementer put the shell renderers in `grid.py`. `Grid` letterbox maths, `_fit_prompt_font`, `_wrap_to_width`, `draw_center_text` binary search are pure and untested. `_LevelBag` difficulty rule ("same or +1, fallback to all") is an ADR-0013 claim with no test. Timer-expiry → BLOWUP → DONE → `"loss"` path untested. +- **Done when:** `grid.py` ~60, `shell.py` ~130, `blowup.py` ~60; tests for `Grid`, `_LevelBag`, blow-up path. + +### P2-08 `body_grid_tester.py` +- **Why:** 626 LOC, half of it P2-01's duplication; reads config keys `grid` and `activation_rule` that exist in no settings file; 3 commits since May; `docs/FundingCAPTCHA.md:249-254` shows an invocation that enters LIVE with no frames. +- **Do:** either `app.py --tester` (debug overlays, opt-in raw/delta payload on `CameraFeed`) or a ~300-LOC standalone on the shared modules. Delete or document the tester-only keys and flags (`--cols/--rows/--debug/--port 8081`, keys `D`/`C`). + +### P2-09 `ShowWeb.LiveServer` +- **Why:** `WebSocketLogHandler`, `/ws`, `/logs`, `/health`, module globals, fan-out loop, uvicorn config are byte-for-byte duplicated between `FB/visualizer.py:312-404` and `TH/visualizer.py:32-184`; `/health` a third time in `Dashboard/serve.py:49-51`. 240 of `FB/visualizer.py`'s 434 lines are an HTML/JS string (`:66-305`); the layout tool next door already uses `StaticFiles`. Dashboard `flowerbeds.html` ≡ `fundingcaptcha.html` except a port; `appendLog/connectLogs` JS repeated ×3. +- **Do:** `ShowControl/ShowWeb/live.py` `LiveServer(logger_name, port, html, extra_routes)` with `start_in_thread()` and `async serve()`, `broadcast()`; `Dashboard/log-panel.js`; `FB/visualizer_static/index.html`. API in core §3a. +- **Done when:** both visualizers <60 LOC Python; no HTML in `.py`. + +### P2-10 `SignalGate` +- **Why:** `OSCFabric/client.py:39-56` and `TH/location_sender.py:129-148` implement the same ADR-0007 change-epsilon + heartbeat rule differently (single vs N targets, `tick(dt)` vs wall clock, float-only vs float+string). `FabricClient.report()` of an unchanged value never refreshes `_elapsed`. +- **Do:** `OSCFabric/gate.py` `SignalGate(heartbeat_interval_s, change_epsilon, clock).due(key, value)/mark()`; both become thin wrappers. + +### P2-11 FlowerBeds `main.py` split +- **Why:** `main.py` mixes config/network loading (×3 copies), camera factory, controller bus + diagnostics (`:74-115`, discovery duplicated at `:92-97` and `layout_tool.py:55-61`), calibration persistence, frame loop, visualizer state assembly, and two motor-calibration CLIs (`:292-411`, 120 LOC, no IIVision). `/cg/ff/rot` + yaw negation defined in both `flower_beds.py:164-167` and `layout_tool.py:71-75`; `_OSC_ADDRESS` in both `main.py:56` and `layout_tool.py:31`. Signal-handler boilerplate ×3 (`:198-206, 331-338, 388-395`). +- **Do:** `controllers.py` (`OSC_ROT_ADDRESS`, `Controller`, `controllers_from_network`, `ControllerBus.send/diagnose`), `calibrate_motors.py` (own entry point), `state.py` (visualizer `*State` TypedDicts move here so `flower_beds.py:21` stops importing the web layer); `config.py` for the dataclasses in `flower_beds.py:34-152`. API in core §3c. +- **Done when:** `main.py` ≈ 180 LOC; layout tool and main share one controller module. + +### P2-12 `IIVision/blob_tracker.py` split +- **Why:** Three concerns in 434 LOC: dataclasses `:29-90`, calibration `:97-216`, detector `:223-434`. `BlobTracker` is a misnomer (per-frame detection; tracking is `BlobStabilizer`); `docs/FlowerBeds.md:11,36` already confuses them. No tests cover `Calibrator.build`, `BlobTracker.detect`, `_compute_3d_blobs`, `transforms.py`, `run_pipeline` — the production CV path. +- **Do:** `types.py`, `calibration.py`, `detector.py` (`BlobDetector`, alias `BlobTracker`); `camera.py` → `orbbec.py` + `mock.py` after P0-12. Add tests for calibrate and detect on synthetic frames. +- **Done when:** each file <220 LOC; CV path has tests. + +### P2-13 `TH/coordinator.py` split — after P0-14 +- **Why:** 9 config dataclasses `:33-125` + 110-line `load_config` `:132-275` + factory `:282-311` + `Coordinator` `:318-485` mixing Fabric intake/stale-sender policy, GardenState assembly, branch math, LED aggregation. `DioramaConfig`/`DormerConfig` mirror `LEDConfig` field-for-field. ADR-0006 wanted `BranchController(Controllable)`; it is a plain serial driver with the math in `Coordinator._update_branch_positions`. +- **Do:** `config.py` (dataclasses with `from_dict`, `load_config`), `garden_state.py` (`SignalIntake`: setters, `_expire_stale_senders`, `snapshot()`; `test_dead_sender.py` targets this and stops asserting private attrs), `branches.py` (`branch_positions()` pure function + `BranchController(Controllable)`), `coordinator.py` ~80 LOC. + +### P2-14 `shadertoy.py` package +- **Why:** Coherent but 468 LOC; `body_blobs.py` imports the renderer module for 6 constants. Low urgency. +- **Do:** `shadertoy/{setup,body,keyboard,blobs_contract,renderer}.py` per fundingcaptcha §4. + +### P2-15 Settings defaults and validation +- **Why:** Every `settings.get(key, default)` carries its own default; `BlobConfig.from_dict` (`body_blobs.py:49-51`) silently drops unknown keys so typos are invisible; `detection.max_depth_mm` is likely a no-op for the foreground path; `slab_styles` readers all hardcode `["0"]`. +- **Do:** `DEFAULTS` dict or dataclass in `settings.py`; warn on unknown keys at startup. + +--- + +## Phase 3 — Docs: one job per file + +| ID | Task | Effort | Gate | Source | +|---|---|---|---|---| +| P3-01 | `docs/repo-map.md` (≤50) replaces `architecture.md` | S | after P0 | docs §7 | +| P3-02 | `docs/osc-fabric.md` (≤50): the one address table; ADR-0007 keeps rationale | S | after P0-16 | docs §3 | +| P3-03 | Collapse `docs/agents/` to `conventions.md`, `firmware.md`, `git-workflow.md` | S | — | docs §7 mapping | +| P3-04 | Element docs: fix listed stale claims, cut to targets | M | after P0/P2 | docs §2 | +| P3-05 | `CONTEXT.md` → glossary only (≤130); add the 5 location names; resolve `Show Mode` and `Bloom` collisions | S | after P0-14 | docs §2 CONTEXT.md | +| P3-06 | ADRs: status line on all, truncate 0004/0009/0011, cut 0015 plan, fix 0010/0017/0018, renumber 0021→0023, 0022→0024, add `adr/README.md` | S | — | docs §4 | +| P3-07 | `showtime.md`/`bootstrap.md` fixes: step 2, `--no-locations`, ESP32 + WiFi provisioning, drop "serials in network.json" | S | — | docs §2 | +| P3-08 | `AGENTS.md` ≤40 lines, links to repo-map and element docs | S | after P3-01 | docs §7 | +| P3-09 | Delete `docs/audit/` once this backlog is in the tracker | S | last | — | + +### P3-04 Element doc fixes (the specific false claims) +- **FlowerBeds.md:** `:11,36` BlobTracker→BlobStabilizer for EMA; `:103` `/flowerbeds/mode` (per D3); `:146` `calibration_state` is constant; absorb module table from `python-conventions.md` and the 6-line pipeline diagram from `architecture.md`. +- **TreeHouse.md:** `:14,194-205` four → five controllers (Garage 192.168.1.64); `:196` controllers ignore GardenState (ADR-0021); `:11,15-17,164-171,218,234` Pico content per D1; `:181,185` dioramas are generic `LEDDisplay`, no attic code exists; `:201-210` add `/lookingglass/*` on :9001. +- **FundingCAPTCHA.md:** `:11,162-170` monitoring page is JSON, not HTML; `:93` `.local.json` is gitignored; `:101` threshold default 0.25 in file; `:108,137` drop keepaway; `:146` pipes_3d is garden vines; `:152` 10 Hz not per-frame; `:158` `/captcha/mode` per D3; `:249-254` tester flags; `:74` verify or remove "not yet verified on Pi 5"; move maintenance mode (`:186-245`) to `showtime.md`; private content (`:273-398`) → ≤30 lines; absorb games table. Target ≤200 lines. +- **Dashboard.md:** `:15,28` relay reaches TreeHouse only; delete `network.json` copy `:52-64`; `:74` state the fact. +- **PlayingThePipes.md:** covered by P1-03. + +### P3-05 CONTEXT.md specifics +- `:85,88` "Display (old name — being replaced)": either P0-14 renamed the classes or delete the note. +- `:58` four dioramas → five locations (Swannatopia, Julia, Jess, Dormer, Garage — none appear today). +- `:94` "4–6 Branches" → settings has 1 motor; `:97` "Bloom" collides with the F&F bloom strip and `bloom.glsl`; no aggregate Bloom exists in code. +- `:128` Signal Bag exists only in C++ firmware, inert since ADR-0021. +- `:47` Games list → BodyCaptcha only. +- `:41` "Show Mode (FundingCAPTCHA)" collides with TreeHouse `ShowMode` enum. +- `:134` FundingCAPTCHA now uses `BlobStabilizer` (ADR-0022). +- `:22-31` and `:91` GardenState fields listed twice; keep one, point at `displays/base.py`. +- `:111-120` OSC table → link to `docs/osc-fabric.md`. + +--- + +## Issues filed (2026-11-09) + +- **#174** Delete 17 stale remote branches. +- **#175** Phase 0 umbrella; sub-issues #180–#197 (P0-03 not filed, no-op after D4/D6). +- **#176** Phase 1 umbrella. **#178** Phase 2 umbrella. **#179** Phase 3 umbrella. Break these out into items when picked up. + +## Counting + +| Phase | Items | Effort (person-days) | Lines removed | +|---|---|---|---| +| 0 | 19 | 3–4 | ~160,000 | +| 1 | 10 | 2–3 | ~300 net (adds READMEs, removes requirements) | +| 2 | 15 | 8–10 | ~1,500 + 350 test boilerplate, moves ~2,000 into smaller files | +| 3 | 9 | 2–3 | ~6,000 of docs | diff --git a/docs/audit/2026-11-09/detail/docs.md b/docs/audit/2026-11-09/detail/docs.md new file mode 100644 index 0000000..e24b32c --- /dev/null +++ b/docs/audit/2026-11-09/detail/docs.md @@ -0,0 +1,466 @@ +# CommunityGarden documentation audit — 2026-11-09 + +Read-only audit of every tracked `.md` (66 files, 7,949 lines) plus the 4 `.html` mockups under `docs/superpowers/specs/` (1,404 lines). Six `.html` files under `ShowControl/Dashboard/` and `ShowControl/FlowerBeds/layout_tool/` are application UI, not docs, and are out of scope. + +Repo history (full clone, 207 commits, first commit 2026-01-24). Code last touched: FundingCAPTCHA 2026-11-08, IIVision 2026-11-05, TreeHouse/Firmware/scripts/PlayingThePipes 2026-10-27, FlowerBeds/Dashboard/OSCFabric 2026-05-29. + +Headline numbers: + +| | files | lines | +|---|---|---| +| All docs audited | 70 | 9,353 | +| Recommend **delete** | 31 | 5,290 (57%) | +| Recommend **merge into another doc** | 12 | ~1,000 | +| Recommend **keep (edit in place)** | 27 | ~3,050 → target ≈ 2,000 | + +--- + +## 1. Inventory + +Audience: **op** = show operator / crew; **dev** = developer; **agent** = AI agent entry/guidance; **hist** = historical / planning artefact. + +| File | LOC | Last commit | Audience | Purpose (one line) | Verdict | +|---|---|---|---|---|---| +| `AGENTS.md` | 32 | 2026-09-06 | agent | Entry point: test cmd, 6 constraints, links to docs/agents | keep, fix 3 claims | +| `CONTEXT.md` | 219 | 2026-05-29 | dev/agent | Domain glossary + OSC table + relationships | keep, cut ~40%, fix 9 stale terms | +| `README.md` | 25 | 2026-05-29 | op/dev | Landing page, element table, doc links | keep | +| `docs/bootstrap.md` | 314 | 2026-09-06 | op | One-time hardware provisioning per machine | keep, fix 4 claims | +| `docs/showtime.md` | 238 | 2026-05-29 | op | Daily ops, services, deploy, troubleshooting | keep (best doc in repo) | +| `docs/Dashboard.md` | 78 | 2026-05-29 | op/dev | Dashboard endpoints, service, network.json copy | keep, cut network.json copy, fix relay claim | +| `docs/FlowerBeds.md` | 131 | 2026-05-29 | op/dev | FlowerBeds architecture, settings, flags, OSC, troubleshooting | keep (canonical element doc) | +| `docs/TreeHouse.md` | 239 | 2026-09-06 | op/dev | TreeHouse architecture, flags, weston, renderer, locations | keep, fix 4→5 controllers | +| `docs/FundingCAPTCHA.md` | 398 | 2026-11-08 | op/dev | Everything FundingCAPTCHA incl. shader/playlist players, private content | keep, cut to ≤200 (move ops to showtime, shrink private-content) | +| `docs/PlayingThePipes.md` | 102 | 2026-05-29 | op | Health server, Max patch, COM ports, deploy | keep, fix maxpat name | +| `docs/PRD.md` | 214 | 2026-05-29 | hist | Pre-festival product requirements (May 2026) | delete (or move to docs/history/) | +| `docs/agents/architecture.md` | 111 | 2026-05-29 | agent | Repo tree (stale) + FlowerBeds-only pipeline | merge → rewrite as 40-line repo map | +| `docs/agents/configuration.md` | 113 | 2026-05-29 | agent | Stale settings.json + network.json copies | merge → element docs; delete copies | +| `docs/agents/domain.md` | 51 | 2026-05-29 | agent | Generic "how to use CONTEXT.md" template from mattpocock/skills | delete | +| `docs/agents/firmware.md` | 35 | 2026-10-27 | agent | Firmware index (4 of 9 dirs) | keep, fix hardcoded-IP claim, add missing dirs → 25 lines | +| `docs/agents/git-workflow.md` | 12 | 2026-09-06 | agent | Branch/PR rules, private-content hook | keep | +| `docs/agents/issue-tracker.md` | 22 | 2026-05-29 | agent | `gh issue` cheat sheet | merge into git-workflow.md | +| `docs/agents/python-conventions.md` | 44 | 2026-05-29 | agent | FlowerBeds module table, coordinate/rotation/logging conventions | keep rotation+logging; move module table to FlowerBeds.md | +| `docs/agents/running-and-testing.md` | 69 | 2026-05-29 | agent | FlowerBeds-only run/test (incomplete flag table) | merge into AGENTS.md (3 lines) + FlowerBeds.md | +| `docs/agents/triage-labels.md` | 15 | 2026-05-29 | agent | Label mapping template, still says "Edit the right-hand column" | delete (or fold 5 lines into git-workflow) | +| `docs/adr/0001` OSC as fabric | 7 | 2026-05-29 | dev | Why OSC/UDP | keep, add status, amend | +| `docs/adr/0002` dwell cluster-local | 14 | 2026-05-29 | dev | Dwell semantics in FlowerCluster | keep | +| `docs/adr/0003` games self-terminate | 9 | 2026-05-29 | dev | Every Game must lose on inactivity | keep | +| `docs/adr/0004` depth touch detection | 37 | 2026-05-29 | hist | Superseded by 0013 (marked) | keep header, truncate body | +| `docs/adr/0005` branch USB serial | 23 | 2026-05-29 | dev | Branch controller JSON-lines over USB | keep | +| `docs/adr/0006` Controllable hierarchy | 33 | 2026-05-29 | dev | Controllable / LEDControllable split | keep, note per-room subclasses never built | +| `docs/adr/0007` OSC fabric schema | 92 | 2026-05-29 | dev | Address table, fire semantics, network.json | keep; mark 5 unimplemented addresses; delete network.json copy | +| `docs/adr/0008` GardenState mapping | 91 | 2026-05-29 | dev | Per-display personality | keep; mark LookingGlass/`overload`/`GardenStateResponse` as not built | +| `docs/adr/0009` ArUco layout | 38 | 2026-05-29 | hist | Superseded by 0015 (marked) | delete-safe (0015 restates context) | +| `docs/adr/0010` Pico LED arch | 69 | 2026-05-29 | hist | Two Picos, udev symlinks | **status wrong** (Accepted → Superseded by 0020) | +| `docs/adr/0011` screen calibration | 36 | 2026-05-29 | hist | Superseded by 0013 (marked) | keep header, truncate body | +| `docs/adr/0012` unified pygame app | 53 | 2026-05-29 | dev | Single app.py process | keep; strike games/pairs.json paragraph | +| `docs/adr/0013` silhouette interaction | 98 | 2026-05-29 | dev | Body Grid, Depth Slabs, Arc state machine | keep (core), add Status | +| `docs/adr/0014` Pipes encoder Picos | 53 | 2026-05-29 | dev | 2 Picos → USB serial → Max | keep | +| `docs/adr/0015` GUI layout tool | 215 | 2026-05-29 | dev | Layout tool design + 110-line done implementation plan | keep, cut plan → ~90 lines | +| `docs/adr/0016` test-input mode | 54 | 2026-05-29 | dev | Mouse-paint depth frames | keep, add Status | +| `docs/adr/0017` club rave screen | 65 | 2026-05-29 | dev | Club diorama 800×480 pygame screen | keep, fix paths | +| `docs/adr/0018` weston dual display | 95 | 2026-05-29 | dev | Single weston, pygame+moderngl | keep, fix 3 claims | +| `docs/adr/0019` game UI shell | 71 | 2026-05-29 | dev | Top bar + maximal grid | keep, add Status | +| `docs/adr/0020` ESP32-S3 controllers | 113 | 2026-10-27 | dev | One ESP32 per location, Garden State over OSC | keep | +| `docs/adr/0021-fundingcaptcha-shader-player` | 30 | 2026-11-05 | dev | Shadertoy Setups on the Pi | keep, **renumber → 0023** | +| `docs/adr/0021-treehouse-controllers-standalone…` | 99 | 2026-10-27 | dev | Controllers ignore Garden State; Garage welder | keep (owns 0021) | +| `docs/adr/0022` blob records | 40 | 2026-11-05 | dev | `blobs` channel source | keep, **renumber → 0024**, fix link | +| `docs/games/bodycaptcha.md` | 81 | 2026-05-29 | dev | BodyCaptcha design | merge into FundingCAPTCHA.md games section (or games/bodycaptcha.py docstring) | +| `docs/games/bodygrid.md` | 46 | 2026-05-29 | dev | Body Grid input layer | merge → body_grid.py docstring; delete | +| `docs/games/keepaway.md` | 120 | 2026-05-29 | dev | Keepaway design (module not loaded) | merge → body_keepaway.py docstring; delete | +| `docs/games/rhythm.md` | 25 | 2026-05-29 | hist | Deleted game, "port TODO" since May | delete (→ GitHub issue if still wanted) | +| `docs/games/upsidedown.md` | 23 | 2026-05-29 | hist | Deleted game, "port TODO" since May | delete (→ GitHub issue) | +| `docs/superpowers/plans/2026-09-06-private-content-sets.md` | 949 | 2026-09-06 | hist | Step-by-step implementation plan with full code listings | delete | +| `docs/superpowers/plans/2026-10-30-endless-sunset.md` | 500 | 2026-10-30 | hist | Implementation plan | delete | +| `docs/superpowers/plans/2026-11-04-hunter-bird.md` | 470 | 2026-11-05 | hist | Implementation plan | delete | +| `docs/superpowers/plans/2026-11-06-watering-can.md` | 424 | 2026-11-06 | hist | Implementation plan | delete | +| `docs/superpowers/specs/2026-09-06-private-content-sets-design.md` | 150 | 2026-09-06 | hist | Design spec, "Approved, pending implementation" (shipped) | delete | +| `docs/superpowers/specs/2026-10-30-endless-sunset-design.md` | 103 | 2026-10-30 | hist | Setup design | delete (salvage 1 line) | +| `docs/superpowers/specs/2026-10-30-endless-sunset-mockup.html` | 149 | 2026-10-30 | hist | Canvas mockup | delete | +| `docs/superpowers/specs/2026-11-04-hunter-bird-design.md` | 118 | 2026-11-06 | hist | Setup design | delete (salvage 1 line) | +| `docs/superpowers/specs/2026-11-04-hunter-bird-mockup.html` | 502 | 2026-11-05 | hist | Mockup (linked from ADR-0022) | delete; fix link | +| `docs/superpowers/specs/2026-11-05-hunter-bird-two-eyes-mockup.html` | 657 | 2026-11-06 | hist | Mockup | delete | +| `docs/superpowers/specs/2026-11-06-watering-can-design.md` | 132 | 2026-11-06 | hist | Setup design incl. "Revisions after first build" | delete (salvage 2 lines) | +| `docs/superpowers/specs/2026-11-06-watering-can-mockup.html` | 96 | 2026-11-06 | hist | Mockup | delete | +| `docs/superpowers/specs/2026-11-08-rainbow-poop-design.md` | 78 | 2026-11-08 | hist | Setup design | delete (salvage 1 line) | +| `Firmware/ESP32S3_SignsOfLife/README.md` | 101 | 2026-09-06 | dev/op | Bring-up/diagnostic firmware for ESP32-S3 boards | keep | +| `Firmware/TreeHouse_Controllers/README.md` | 174 | 2026-10-27 | dev/op | Build, flash, self-test, retune the 5 location controllers | keep (canonical) | +| `Prototypes/GlowPanels/README.md` | 58 | 2026-05-29 | hist | Mycelium glow-panel prototype comparison | delete with `Prototypes/GlowPanels/` (or keep if the physical prototyping continues) | +| `Prototypes/GlowPanels/01_SideLit/README.md` | 137 | 2026-05-29 | hist | Build notes, prototype 1 | delete | +| `Prototypes/GlowPanels/02_BackLit_Frosted/README.md` | 169 | 2026-05-29 | hist | Build notes, prototype 2 | delete | +| `ShowControl/FundingCAPTCHA/distribution/README.md` | 85 | 2026-09-30 | dev | Build/ship Windows level editor, merge contributions | keep | +| `ShowControl/FundingCAPTCHA/shaders/README.md` | 91 | 2026-11-05 | dev (shader authors) | Shadertoy prototyping rules + hand-in | keep (canonical) | +| `ShowControl/FundingCAPTCHA/videos/README.md` | 8 | 2026-11-08 | op | Where video files go | keep | +| `ShowControl/PlayingThePipes/MakeNoiseDay/other/README.md` | 10 | 2026-09-25 | — | Vendored third-party README ("WineAndWiresMax", Twitch/YouTube links) | delete | + +--- + +## 2. Accuracy audit — stale or false claims + +Every claim below was checked against the working tree. Format: `doc:line` → what the code actually does. + +### AGENTS.md (the agent entry point — these matter most) + +- **L8** "`make test` runs Node tests (FundingCAPTCHA)" → There are no JS tests. The only `.test.js` ever (`ShowControl/FundingCAPTCHA/touch-input.test.js`) was deleted 2026-05-11 ("delete browser stack"). The only JS file in the repo is `ShowControl/FlowerBeds/layout_tool/layout.js`. `Makefile` still runs `node --test`, which finds nothing. +- **L13** "`IIVision.Blob3D.world_pos_cm()` converts between them" → `Blob3D` (IIVision/blob_tracker.py:76) has no such method. The conversion is `IIVision.transforms.orbbec_to_world()` (transforms.py:62). `world_pos_cm` is a *field* on `TrackedBlob`. +- **L15** constraint 3: "firmware reads a generated `config.h`" → `Firmware/FlowerBeds_Follow_ServoController/FlowerBeds_Follow_ServoController.ino:25-26` hardcodes `byte mac[] = {0xDE,0xAD,0xBE,0xEF,0x15,0x01}` and `IPAddress ip(192,168,1,51)` and includes no `config.h`. `scripts/hooks/firmware_config_gen.py:20` says `TODO: generate Firmware/FlowerBeds_Follow_ServoController/config.h too.` Only `Firmware/TreeHouse_Controllers/include/net_config.h` is generated. The repo's own constraint is violated by the repo's own firmware, and three docs (AGENTS, configuration.md:113, firmware.md:9, ADR-0007:212) assert otherwise. + +### docs/agents/architecture.md + +- **L5-26 repo tree** is missing: `Firmware/{Club_Pico, Fungi_Pico, HouseSwarming_Pico, PlayingThePipes_EncoderPico, ESP32S3_SignsOfLife, TreeHouse_Controllers}` (6 of 9 firmware dirs), `ShowControl/PlayingThePipes/`, `Prototypes/`, `scripts/{deploy.sh, bootstrap-deploy.sh, install-services.sh, install-git-hooks.sh, git-hooks/}`, `requirements-dev.txt`, `docs/{games,superpowers}`. +- **L25** "pytest.ini testpaths = ShowControl IIVision" → actual: `ShowControl IIVision scripts`. +- **L33-40 IIVision table** omits `silhouette_warp.py` (`reproject_silhouette`), `OrbbecRGBCamera`/`RGBFrame`, `Calibrator`, `DetectionConfig`, `filter_positions_near_points`, `PreStabilizeFilter`, `update_tracks()` (ADR-0022). +- **Whole file**: titled "Architecture & Pipeline" but L47-111 is FlowerBeds only. TreeHouse, FundingCAPTCHA and Pipes architecture live in their `docs/.md`. An agent reading this gets a 1-of-4 picture. + +### docs/agents/configuration.md + +- **L7-70** FlowerBeds `settings.json` example diverges from the real file: real has `activity_max_blobs`, `_comment` keys, camera `mirror: true`, `pos_cm [-484,0,290]`, `rotation pitch -30 / yaw 90`; the documented `"serial"` key (L64, L75) is **not a field** of `CameraConfig` (flower_beds.py:139-152) and is silently dropped. `yaw_limit_deg` — the one per-cluster key operators actually tune — is absent. +- **L96-110** `network.json` copy is missing `lan`, `change_epsilon`, and the five `treehouse_{swannatopia,julia,jess,dormer,garage}` entries (5 of 8 firmware entries). +- **L113** "Firmware reads addresses from a generated `config.h`" → false for FlowerBeds (see AGENTS L15). +- Title says "per Element" but covers only FlowerBeds; nothing on `TreeHouse/settings.json` (12 top-level keys) or `captcha-settings.json`. + +### docs/agents/running-and-testing.md + +- **L6** "Node tests (FundingCAPTCHA)" → none exist. +- **L37-47** flag table omits `--recalibrate`, `--calibrate-yaw-sweep`, `--sweep-period-s` (main.py:421-437). `docs/FlowerBeds.md:80-91` has the complete, correct table. +- FlowerBeds-only; TreeHouse/FundingCAPTCHA dev commands are in `showtime.md:154-171`. + +### docs/agents/python-conventions.md + +- **L20** "`Blob3D.world_pos_cm()` converts camera → world" → `orbbec_to_world()` (transforms.py:62). Mapping on L21-23 is correct. +- **L29** rotation order ✓ matches `rotator_to_matrix` (`"yxz"`: roll, pitch, −yaw). The yaw negation is not mentioned; worth one line. +- **L44** "never `print`" → `layout_tool.py`, `visualizer.py`, `bodycaptcha_editor.py`, `distribution/*.py` all use `print`. Either the rule is wrong or scoped to the frame loop. +- Logger name `"flower_beds"` ✓; other Elements use `treehouse`, `captcha`, `osc_fabric`, `shader_app`, `pipes-health` — doc doesn't say. + +### docs/agents/firmware.md + +- **L9** FlowerBeds "IP and MAC come from a generated `config.h`" → hardcoded (see above). +- Missing entirely: `Club_Pico`, `Fungi_Pico`, `HouseSwarming_Pico` (standalone "stopgap" MicroPython controllers — their docstrings say so; undocumented anywhere), `PlayingThePipes_EncoderPico` (only mentioned in PlayingThePipes.md), `ESP32S3_SignsOfLife` (has its own README). 5 of 9 firmware dirs undocumented here. +- L18-31 ESP32 content is accurate but duplicates `Firmware/TreeHouse_Controllers/README.md`. + +### docs/agents/domain.md + +- **Entire file** is an unmodified template from mattpocock/skills: references `CONTEXT-MAP.md`, `src//docs/adr/`, `/grill-with-docs`, example ADRs "0001-event-sourced-orders", "0002-postgres-for-write-model". None exist here. Zero repo-specific content. The title "Domain glossary & vocabulary" in AGENTS.md is misleading — the glossary is `CONTEXT.md`. + +### docs/agents/triage-labels.md + +- L5 header "Label in mattpocock/skills"; L15 "Edit the right-hand column to match whatever vocabulary you actually use" — template instruction left in. Label existence on GitHub not verifiable from this session (GraphQL blocked). + +### docs/agents/issue-tracker.md + +- L14 "see the global instructions for the `gh pr create` example" → dangling reference; the example is in git-workflow.md. + +### CONTEXT.md + +- **L85** Controllable: "_Avoid_: 'Display' (the old name — being replaced)" and **L88** "_Avoid_: 'LEDDisplay' (the old name — being replaced)" → rename is half-done and has been for 5+ months. Base classes are `Controllable`/`LEDControllable` (displays/base.py:51,73) but **every concrete class is still `*Display`**: `LEDDisplay` (led.py:22), `ForgeAndFloraDisplay` (effect.py:22), `PorchLightsDisplay` (porch_lights.py:26), `LookingGlassDisplay` (video.py:20); the package is `displays/`. The glossary forbids the word the code uses. +- **L58** "Four dioramas exist: House Swarming, Club, Mycelium, Forge & Flora" → `settings.json` has 3 `dioramas` + Forge & Flora under `garage_windows`. More importantly the house is now organised as five **locations** — Swannatopia, Julia, Jess, Dormer, Garage (ADR-0020/0021, network.json, settings.json `locations.controllers`) — and **none of those five names appears in CONTEXT.md**. +- **L94** Branch "4–6 motorised branches" → `settings.json branch_controller.motors` has 1 motor. **L97** "Bloom: 0.0–1.0 value… aggregate extension state of all Branches" → no such concept in code; `coordinator.py:453-466` computes a per-motor weighted `signal` → degrees between `min_pos`/`max_pos`, with `recoil_pos` on blowup. In code "bloom" means the Forge & Flora bloom strip (`bloom_pin`) and a Looking Glass shader (`bloom.glsl`). The glossary's "Bloom" actively collides with two real things. +- **L128** Signal Bag: "pattern used inside individual Controllable implementations" → exists only in C++ firmware (`Firmware/TreeHouse_Controllers/lib/Patterns/Patterns.h`); no Python Controllable has one; and ADR-0021 made the weights inert. +- **L47** Game: "Examples: BodyCaptcha, UpsideDown, Rhythm, Keepaway. `app.py` loads all Games" → `app.py:349-351 _load_games` loads `("bodycaptcha",)` only. UpsideDown/Rhythm code was deleted (ADR-0013, 2026-05); Keepaway exists but is not loaded (#142). +- **L41** "Show Mode (FundingCAPTCHA)" = automated game rotation → collides with TreeHouse `ShowMode` enum (`active`/`dim`/`inactive`, base.py:10) which is a `GardenState` field (L26, L91 of the same file). Same term, two meanings, in the file whose job is to prevent that. +- **L134** IIVision "used by FundingCAPTCHA (background subtraction + denoising only — blob detection not used)" → since ADR-0022 (2026-11-05) `body_blobs.py` runs IIVision `BlobStabilizer.update_tracks()` for the shader player. +- **L22-31 and L91**: GardenState fields listed twice in the same file. +- **L111-120** OSC table ✓ matches `osc_server.py` but omits `/lookingglass/{scene,time,intensity}` (osc_server.py:8, TreeHouse.md:118-122) and `/cg/ff/{rot,ping,pong,hb}`. +- L75 Attic TV "8 LEDs" ✓; L64 Mycelium "224 LEDs" ✓; L43 pipes screensaver ✓; L111-120 ✓. + +### docs/showtime.md + +- **L51-57** "Daily startup" list numbered 1, 3, 4, 5, 6 — step 2 missing. +- **L160** TreeHouse no-hardware command omits `--no-locations` (TreeHouse.md:46), so it will UDP-spam 192.168.1.60-64. +- Everything else checked (ports, service names, deploy.sh usage, journalctl, state machine) ✓. This is the most accurate doc in the repo. + +### docs/bootstrap.md + +- **L13** treehouse hardware "2× Pi Pico, 1× branch controller, 2× HDMI" → omits the 5× ESP32-S3 WiFi location controllers (192.168.1.60-64), which are now the primary lighting path. Nowhere in bootstrap/showtime is the WiFi network they join provisioned (the show LAN is described as wired, no DHCP; `secrets.h` holds credentials for an AP that is not documented). +- **L127** "Record the serials in `ShowControl/network.json` under `firmware`" → `network.json` has no `serial` field; schema is `{ip, mac, osc_port}`. +- **L173, L198** `PlayingThePipes.maxpat` → does not exist. Actual: `Lamplight2026/Lamplight2026.maxproj`, `Pipes Prototype 3/Pipes Prototype 3.maxproj`, `MakeNoiseDay/MakeNoiseDay.maxproj`, `EncoderTester.maxpat`. +- L73 FlowerBeds install.sh has no udev rule ✓ (confirmed). L94-97 TreeHouse apt list ✓. + +### docs/Dashboard.md + +- **L15, L28** `POST /api/mode` "relays show mode to a specific Element over OSC" → only TreeHouse has an OSC server (`osc_server.py`). FlowerBeds `main.py`, FundingCAPTCHA `app.py` and Pipes have **no OSC listener**. The relay to 3 of 4 elements is a no-op. L78 hedges ("Element doesn't listen") where it should state the fact. +- **L52-64** `network.json` copy stale (missing `mac`, `lan`, `change_epsilon`, `treehouse_*`). +- L5 `cg-dashboard`, port 9000 ✓ (serve.py:96, install.sh:31). + +### docs/FlowerBeds.md + +- **L103** `/flowerbeds/mode | in (Fabric) | relayed by Dashboard` → nothing in FlowerBeds listens for OSC. +- **L11, L36** "cross-frame stabilisation (`BlobTracker` with EMA smoothing)" → that is `BlobStabilizer`; `BlobTracker` is per-frame detection. +- Flags table L80-91 ✓ exact match to main.py:416-437. Layout tool flags ✓ (layout_tool.py:219-224). `yaw_limit_deg` default 60 ✓. `/cg/ff/{ping,pong}` ✓. + +### docs/TreeHouse.md + +- **L14** "4× ESP32-S3 location controllers — Swannatopia, Julia, Jess, Dormer" and **L194-205** table of 4 → there are **5** (Garage, 192.168.1.64; `settings.json locations.controllers` lists 5; `platformio.ini` has `env:garage`; ADR-0021). +- **L196** describes controllers following Garden State; omits that they currently ignore it (ADR-0021, `kFollowGardenState=false`). firmware.md:23 says so; TreeHouse.md doesn't. +- Not mentioned: `looking_glass/deploy/looking_glass.service` + `install.sh` still in tree though ADR-0018:1108 says "obsolete and must not be installed". +- Flags L40-51 ✓ (main.py:32-43). weston.ini L73-91 ✓ byte-identical. GardenState L182-188 ✓. + +### docs/FundingCAPTCHA.md + +- **L158** `/captcha/mode | s (in)` → app.py has no OSC listener; only outbound `FabricClient` (app.py:478). +- **L101** `cell_activation_threshold` "default 0.30" → `captcha-settings.json` ships 0.25 (code default is 0.30). +- **L61** links to four `docs/superpowers/specs/*-design.md` — breaks if specs are deleted. The tunable names it lists are already at the top of each `common.glsl`. +- L5, L42-46, L65-71, L87, L96-108, L135-138, L146 all ✓ against app.py / shader_app.py / playlist_app.py / captcha-settings.json / games/ / ScreenSavers/. +- Structural: L186-245 (maintenance mode) and L273-398 (private content, 125 lines) are operations prose; the private-content section documents 3 commands and 2 env vars. + +### docs/PlayingThePipes.md + +- **L11, L43, L93** `PlayingThePipes.maxpat` → does not exist (see bootstrap). The directory's actual content (Lamplight2026, Pipes Prototype 3, MakeNoiseDay — last touched 2026-10-27) is not mentioned. +- **L57-60** COM-port table still "TBD". +- L13 "does not yet join the OSC Fabric" ✓ consistent with code. L17 health_server 55 lines ✓, port 8767 ✓. `Firmware/PlayingThePipes_EncoderPico/` ✓ exists. + +### docs/games/*.md + +- **bodygrid.md:100** "`BodyGridActivator` class (in `games/grid.py`)" → it is in `body_grid.py:47`. +- **bodycaptcha.md:4** "Status: In implementation (2026-05-13)" → shipped; **L30** "hold progress bar" → replaced by hold-ring (ADR-0019). Config block L71-78 lacks `hold_decay_rate` (real file has it). +- **keepaway.md:135** "Implemented as … body_keepaway.py" → true but not loaded by app.py (:349-351); doc doesn't say. "designed to be winnable" (L141) sits oddly against CONTEXT "no overall victory condition". +- **rhythm.md, upsidedown.md**: describe code deleted 2026-05; "Port TODO" open 6 months with no issue linked. + +### ADRs — factual errors (status review is in §4) + +- **0001:8** "addresses configured statically in each Element's `settings.json`" → superseded by `network.json` (ADR-0007). +- **0007:154,159-162** `/captcha/game_started`, `/flowerbeds/mode`, `/captcha/mode`, `/captcha/restart` (OSC), `/pipes/mode` → **never implemented**; no OSC listener exists outside TreeHouse. **0007:155** intensity "`level / total_levels`" → now weighted difficulty+time (CONTEXT L72). **0007:212** `config.h` → false for FlowerBeds. **0007:195-210** network.json copy stale. +- **0008:264-282** LookingGlass mood-mirror scene selection and `overload` scene → **not built**: `video.py:36 SCENES` is still the four scenes; `update()` (video.py:67-75) just sends the configured scene and a mean intensity. **0008:284-294** `GardenStateResponse` config block → does not exist; `LEDDisplay.update()` (led.py:60-100) selects pattern from static config. Branches ✓ (coordinator.py:453-466 incl. recoil), ForgeAndFlora ✓ (effect.py:78), Porch ✓. +- **0006:133** "Each room becomes its own class (`HouseSwarmingDisplay`, `ClubDisplay`, `AtticDisplay`)" → never built; all use generic `LEDDisplay`. +- **0010:412** "Udev rules are generated by `scripts/install-services.sh` using serial numbers recorded in `network.json`" → neither exists (install-services.sh has no udev; network.json has no serials; bootstrap.md:99-129 says do it by hand). +- **0012:507-511** games "UpsideDown, Rhythm, Keepaway", `pairs.json`, `rhythm-images.json`, `keepaway-images.json` → none of these files exist. +- **0017:976, 978, 1016-1017** paths `treehouse/club_screen.py` → `ShowControl/TreeHouse/club_screen.py`. +- **0018:1057** "`weston.ini` (in `looking_glass/deploy/`)" → `ShowControl/TreeHouse/deploy/weston.ini`. **0018:1062** `shell=kiosk-shell.so` → actual file uses `desktop-shell.so` with a `[shell]` block. **0018:1108** "`looking_glass.service` is obsolete and must not be installed" → still tracked at `looking_glass/deploy/`. +- **0020:1217-1219, 1276-1281** "four locations", "four environments" → amended to five by 0021 (header says so ✓; body not updated). + +### In-tree READMEs + +- `Firmware/TreeHouse_Controllers/README.md` — all checked claims ✓ (5 envs + 5 selftest + native in platformio.ini:50-104; IPs match network.json; `src/targets/{dormer,garage,jess,julia,swannatopia}.h`). +- `Firmware/ESP32S3_SignsOfLife/README.md` ✓. +- `shaders/README.md` ✓ (setups.json lists 5; `blob_debug/`, `blob_painter.glsl`, `body_painter.glsl` exist). +- `distribution/README.md` ✓ (build.py, package.py, merge_levels.py, TEAMMATE_README.txt, BodyCaptchaEditor.command exist). +- `Prototypes/GlowPanels/README.md` tree ✓ (`firmware/panel_gradient.py` exists). +- `PlayingThePipes/MakeNoiseDay/other/README.md` — third-party content, nothing to check. + +--- + +## 3. Redundancy map + +| Fact | Stated in | Canonical home | Delete from | +|---|---|---|---| +| OSC Fabric address table | CONTEXT.md:111-120; ADR-0007:151-163; TreeHouse.md:213-223; firmware.md:25; ADR-0020:1246-1253; FlowerBeds.md:97-103; FundingCAPTCHA.md:154-158; `osc_server.py` docstring | **`ShowControl/TreeHouse/osc_server.py` docstring** (it's the receiver) + one table in `docs/osc-fabric.md` (new, ≤40 lines, replaces the schema half of ADR-0007) | CONTEXT.md (link instead), firmware.md, ADR-0020 body; element docs keep only *their* outbound rows | +| `network.json` contents verbatim | configuration.md:96-110; ADR-0007:195-210; Dashboard.md:52-64 — all three stale | **The file itself** (it already carries a `_comment`) | all three | +| World coordinate system | AGENTS.md:10; python-conventions.md:14-25; configuration.md:76-78; ADR-0015:736; `transforms.py` docstring | **AGENTS.md one line** + `IIVision/transforms.py` docstring | configuration.md; trim python-conventions to rotation convention | +| FlowerBeds CLI flags | running-and-testing.md:37-47 (incomplete); FlowerBeds.md:80-91 (complete); showtime.md:163 | **FlowerBeds.md** | running-and-testing.md | +| Layout tool usage | architecture.md:60-79; configuration.md:80-90; running-and-testing.md:49-61; FlowerBeds.md:42-74; ADR-0015 | **FlowerBeds.md** | architecture, configuration, running-and-testing | +| FlowerBeds pipeline narrative | architecture.md:47-107; FlowerBeds.md:9-15; PRD §3.1; CONTEXT.md FlowerBeds domain | **FlowerBeds.md** (+ `flower_beds.py` docstring already explains Attraction) | architecture.md (keep a 6-line pipeline diagram at most) | +| GardenState fields | CONTEXT.md:22-31 **and** :91 (same file twice); TreeHouse.md:182-188; ADR-0008; `displays/base.py:33` | **`displays/base.py` dataclass**; CONTEXT.md one entry | CONTEXT.md second copy; TreeHouse.md → "see base.py" | +| Glossary | CONTEXT.md vs docs/agents/domain.md | **CONTEXT.md** | domain.md (it isn't a glossary) | +| No-hardware dev commands | AGENTS.md:17; running-and-testing.md:19-23; showtime.md:154-171; each element doc | **element docs**; AGENTS.md keeps one FlowerBeds line | running-and-testing.md | +| Depth Slab / Body Grid config shape | CONTEXT.md:57-66; ADR-0013:556-566; games/bodygrid.md:116-126; FundingCAPTCHA.md:99-101 | **FundingCAPTCHA.md settings section** | bodygrid.md; CONTEXT.md keeps the definition, drops the JSON | +| Hosts / IPs / ports / service names | showtime.md:11-33; bootstrap.md:11-18; each element doc header; Dashboard.md:5 | **showtime.md machine map** | bootstrap.md table (link); element docs keep one header line | +| Private content sets | FundingCAPTCHA.md:273-398; git-workflow.md:3; AGENTS.md:21; spec+plan (1,099 lines); `.gitignore` comment; pre-commit header | **AGENTS.md rule** + FundingCAPTCHA.md ≤30 lines | spec, plan; shrink FundingCAPTCHA section | +| Pico A/B channel assignment | TreeHouse.md:16-17, 168-172; bootstrap.md:103-107; ADR-0010:382-384; PRD §3.2 | **`TreeHouse/settings.json`**; TreeHouse.md one table (hardware is being retired) | bootstrap.md, PRD | +| Captcha maintenance / console access | FundingCAPTCHA.md:186-245; showtime.md:203-210 | **showtime.md** | FundingCAPTCHA.md | +| ESP32 controller facts (5 envs, standalone, `kFollowGardenState`) | firmware.md:18-31; TreeHouse.md:194-207; `Firmware/TreeHouse_Controllers/README.md`; ADR-0020/21 | **`Firmware/TreeHouse_Controllers/README.md`** | firmware.md → 3 lines + link; TreeHouse.md → table + link | +| Test command | AGENTS.md:8; running-and-testing.md:6; Makefile | **Makefile** + AGENTS.md one line | running-and-testing.md | + +--- + +## 4. ADR review + +Status check done against the code. "No Status" = file has no `**Status:**` line. + +| ADR | In force? | Status field | Recommendation | +|---|---|---|---| +| 0001 OSC as fabric | Yes, but L8 (addresses in `settings.json`) superseded by 0007 | none | **keep**; add `Status: Accepted; addressing amended by ADR-0007` | +| 0002 Dwell cluster-local | Yes (`flower_beds.py:42-61`) | none | **keep**; add status | +| 0003 Games self-terminate | Yes | none | **keep**; add status | +| 0004 Depth touch detection | Superseded (marked) | ✓ | **keep header, truncate body** to the superseded notice + 2 lines (37 → ~6) | +| 0005 Branch USB serial | Yes (`branch_controller.py` JSON lines) | ✓ | **keep** | +| 0006 Controllable hierarchy | Yes, but per-room subclasses (L133) never built | ✓ | **keep**; add note "per-room subclasses not built; generic `LEDDisplay` + config used instead" | +| 0007 OSC schema | **Partially** — 5 of 11 addresses never implemented; intensity semantics changed; network.json copy stale; `config.h` false | says Accepted | **keep, amend**: mark unimplemented rows, delete JSON copy, fix L212; or split: schema → `docs/osc-fabric.md`, keep ADR as rationale only | +| 0008 GardenState mapping | **Partially** — Branches ✓, F&F ✓, Porch ✓; LookingGlass mood-mirror + `overload` ✗; `GardenStateResponse` ✗. Also whole Pi-side LED path now superseded by 0020 and reactivity disabled by 0021 | says Accepted | **keep, mark** "Partially implemented; LED sections superseded by ADR-0020/0021" | +| 0009 ArUco layout | Superseded (marked) | ✓ | **delete-safe** — 0015 restates the context fully; else truncate like 0004 | +| 0010 Pico LED architecture | **Superseded by 0020** (0020 says so; 0010 doesn't). L412 udev-generation never built | **wrong: says Accepted** | **fix status → Superseded by ADR-0020**; truncate | +| 0011 Screen calibration | Superseded (marked) | ✓ | **keep header, truncate body** | +| 0012 Unified pygame app | Yes | none | **keep**; add status; strike L507-511 (UpsideDown/Rhythm/pairs.json never existed in this form) | +| 0013 Silhouette interaction | Yes — core design | none | **keep**; add `Status: Accepted; Supersedes 0004, 0011`; note L624 (Keepaway ported but unloaded; UpsideDown/Rhythm not ported) | +| 0014 Pipes encoder Picos | Yes (firmware exists; Max side unverified) | ✓ | **keep** | +| 0015 Layout tool | Yes | ✓ | **keep, cut** L822-888 implementation plan (done) → ~90 lines | +| 0016 `--test-input` | Yes (app.py:413-416) | none | **keep**; add status | +| 0017 Club screen | Yes | ✓ | **keep**; fix paths | +| 0018 weston dual display | Yes | ✓ | **keep**; fix weston.ini path, `desktop-shell.so`, and remove or acknowledge `looking_glass/deploy/looking_glass.service` | +| 0019 Game UI shell | Yes | none | **keep**; add status | +| 0020 ESP32 location controllers | Yes, amended | ✓ | **keep**; update "four" → "five" in body (header already says so) | +| **0021** TreeHouse standalone + Garage | Yes | ✓ | **keep**, owns number 0021 (committed 2026-10-27, first) | +| **0021** FundingCAPTCHA shader player | Yes, amended by 0022 | none | **renumber → 0023** (committed 2026-11-05, second). Update links in: `0022` header, `docs/FundingCAPTCHA.md:32`, `shaders/README.md` (none), `ADR-0022:1444` | +| 0022 Blob records | Yes | ✓ | **renumber → 0024** (so it follows what it amends); fix `superpowers/...mockup.html` link at L1448 → delete the link or replace with `shaders/hunter_bird/` | + +**Numbering collision**: two files named `0021-*`. Both are current. Rename shader-player → `0023-fundingcaptcha-shader-player.md` and blob-records → `0024-shader-player-blob-records.md`; three link edits. Add a one-line `docs/adr/README.md` index so the next collision is visible. + +**ADRs with no Status line** (10): 0001, 0002, 0003, 0012, 0013, 0016, 0019, 0021-shader, plus 0004/0011 use a blockquote instead of the field. Standardise on `**Status:**` as the first line under the title. + +--- + +## 5. Superpowers specs & plans + +All five features are shipped and tested: + +| Feature | Shipped code | Tests | Spec | Plan | Mockups | +|---|---|---|---|---|---| +| Private content sets | `app.py --levels`, `arc.py`, `deploy/install.sh` sync, `scripts/git-hooks/pre-commit`, `.gitignore` | `test_bodycaptcha_levels.py`, `test_app_games.py`, `test_bodycaptcha_editor.py`, `scripts/test_git_hooks.py` | 150 ("Approved, pending implementation" — wrong, shipped 2026-09-06) | 949 | — | +| Endless sunset | `shaders/endless_sunset/{setup.json,common.glsl,image.glsl,…}`; in `setups.json`, `playlist.json` | `test_endless_sunset.py` | 103 | 500 | 149 | +| Hunter bird | `shaders/hunter_bird/`; `body_blobs.py`; `BlobStabilizer.update_tracks()`; ADR-0022 | `test_hunter_bird.py`, `test_body_blobs.py` | 118 | 470 | 502 + 657 | +| Watering can | `shaders/watering_can/` | `test_watering_can.py` | 132 | 424 | 96 | +| Rainbow poop | `shaders/rainbow_poop/` | `test_rainbow_poop.py` | 78 | — | — | + +**Recommendation: delete all 13 files (4,328 lines, 46% of all doc lines).** The plans are superpowers-workflow transcripts with full code listings that now diverge from the shipped code. The specs' "decided" sections are restated in the shipped `common.glsl` headers and tests. Two inbound links must be fixed: `docs/FundingCAPTCHA.md:61` (four spec links) and `docs/adr/0022…:1448` (mockup link). + +Worth salvaging (one line each, into the named destination): + +- **private-content-sets-design.md §"Failure behaviour"** (L119-135) — the fallback contract (bad `--levels` → single built-in level, never refuse to start; mid-run bad reload keeps old levels) → already in `FundingCAPTCHA.md:316-327`; nothing to salvage. +- **endless-sunset-design.md §"Anti-goals"** (L25-30) — what the Setup must *not* do (e.g. no outline/fill of the body, no text) → one line in `shaders/endless_sunset/common.glsl` header. +- **hunter-bird-design.md §"Behaviour"** — the eye locks one person at a time, strikes once per person → one line in `shaders/hunter_bird/common.glsl` header (the blob-records *contract* is already ADR-0022). +- **watering-can-design.md §"Revisions after the first build"** (L127-132) and **§"Testing (decided)"** (L100-118) — the sideways-step-rewinds rule and the testing harness pattern (`ctx` fixture copied from `test_shadertoy.py`) → one line in `common.glsl` header; the fixture pattern deserves one sentence in `shaders/README.md` §"Hand it in" ("add a `test_.py` modelled on `test_watering_can.py`"). +- **rainbow-poop-design.md §"Revisions after the first build"** (L71-78) → one line in its `common.glsl` header. +- **Nothing** from the four plans or the three HTML mockups. + +If the team wants a place for future design-before-build artefacts, that place is a GitHub issue or PR description (git-workflow.md already says PRs carry the summary), not a tracked directory. + +--- + +## 6. docs/PRD.md + +**What it is**: a pre-festival Product Requirements Document, "Version 1.0", for the Connect Beyond Festival debut 29–31 May 2026. Created 2026-04-29, last substantively edited 2026-05-22/23, 214 lines. + +**Is it current**: No. Every §3 "Status" line is a May-2026 snapshot ("Interaction pivot complete (2026-05-12)", "Physical structure under construction"). Concrete stale claims: §3.2 LED hardware = two Picos (superseded by ESP32-S3, ADR-0020); §3.3 "Pi 5 has no analog audio" in Open Q6 vs §3.3 body "Windows mini PC"; §3.4 lists `pairs.json`, `rhythm-images.json`, `keepaway-images.json` (don't exist) and says "/captcha/intensity … sent every 100ms" (sent on-change + heartbeat); §5 "static IPs hardcoded in `settings.json`" (contradicts `network.json`, ADR-0007); §6 Monitoring Dashboard described as TBD (built); Open Questions 7 and 8 still "TBD" six months after the festival. The festival it was written for is five months in the past; the installation has since toured (Carolina tweaks #141, Lamplight 2026 #155). + +**Who reads it**: nobody operationally. The thematic intent (§1) and spatial layout (§2) are the only parts with lasting value and they are duplicated in `CONTEXT.md` (Elements section) and `README.md`. + +**Recommendation**: delete. If the team wants the thematic-intent table and the layout diagram kept, move those ~30 lines into `README.md` (it already has the element table) and delete the rest. Do not keep a "living document" that hasn't lived since May. + +--- + +## 7. Proposed target structure + +Principle: one file per *job*; operators read `docs/`, developers read `docs/.md` + module docstrings, agents read `AGENTS.md` and follow links. Facts live where the code is. + +``` +CommunityGarden/ +├── README.md (25) what this is, element table, links to docs/ — keep as is +├── AGENTS.md (≤40) agent entry: test cmd, constraints, repo map link, doc index +├── CONTEXT.md (≤130) glossary ONLY: terms + avoid-lists; no OSC table, no GardenState fields, no JSON +├── docs/ +│ ├── showtime.md (≤240) daily ops, machine map, services, deploy, troubleshooting (+captcha maintenance mode) +│ ├── bootstrap.md (≤300) one-time provisioning (+ESP32 WiFi/AP, remove serials-in-network.json) +│ ├── repo-map.md (≤50) NEW: accurate directory tree with one line per dir — replaces architecture.md +│ ├── osc-fabric.md (≤50) NEW: the one address table (implemented vs planned), fire semantics, network.json schema — schema half of ADR-0007 +│ ├── FlowerBeds.md (≤150) element doc (+ module table from python-conventions) +│ ├── TreeHouse.md (≤220) element doc (5 locations; link Firmware README) +│ ├── FundingCAPTCHA.md (≤200) element doc (ops prose → showtime; private content ≤30 lines; games table absorbs games/*.md) +│ ├── PlayingThePipes.md (≤90) element doc (real Max project names) +│ ├── Dashboard.md (≤50) endpoints + "relay only reaches TreeHouse" +│ ├── agents/ +│ │ ├── conventions.md (≤40) coordinate + rotation + logging + print rule (from python-conventions) +│ │ ├── firmware.md (≤30) index of all 9 Firmware/ dirs, one line each + links +│ │ └── git-workflow.md (≤25) branches, PRs, gh issue cheat-sheet, labels (absorbs issue-tracker, triage-labels) +│ └── adr/ +│ ├── README.md (≤30) NEW: numbered index with status column +│ └── 0001…0024 (~900 total after truncating 0004/0009/0011, cutting 0015 plan, renumbering) +├── IIVision/README.md (≤25) NEW stub +├── ShowControl/OSCFabric/README.md(≤15) NEW stub +├── ShowControl/TreeHouse/displays/… (docstrings; no README needed) +├── ShowControl/FundingCAPTCHA/ +│ ├── shaders/README.md (91) keep +│ ├── distribution/README.md (85) keep +│ ├── videos/README.md (8) keep +│ └── games/README.md (≤20) NEW stub (replaces docs/games/) +├── Firmware/ +│ ├── README.md (≤20) NEW stub: which dir is live, which is stopgap, which is superseded +│ ├── TreeHouse_Controllers/README.md (174) keep +│ └── ESP32S3_SignsOfLife/README.md (101) keep +└── scripts/README.md (≤15) NEW stub +``` + +Target: ~30 files, ~3,000 lines (from 70 files / 9,353). + +### Mapping old → new + +| Old | Action | New | +|---|---|---| +| `AGENTS.md` | keep, fix L8/L13/L15, relink | `AGENTS.md` | +| `CONTEXT.md` | keep, cut OSC table, GardenState copy, Signal Bag, Bloom, Branch count, Game list; add Location/Swannatopia/Julia/Jess/Dormer/Garage/Setup/Body/Playlist; resolve Display rename (either rename classes or drop the avoid-note); resolve Show Mode collision | `CONTEXT.md` | +| `README.md` | keep | `README.md` | +| `docs/showtime.md` | keep; fix numbering, add `--no-locations`; absorb FundingCAPTCHA maintenance mode | `docs/showtime.md` | +| `docs/bootstrap.md` | keep; fix maxpat, serials, add ESP32 controllers + WiFi | `docs/bootstrap.md` | +| `docs/agents/architecture.md` | **merge**: tree → `repo-map.md`; FlowerBeds pipeline → `FlowerBeds.md` (6-line diagram) | delete | +| `docs/agents/configuration.md` | **merge**: settings prose → `FlowerBeds.md` (it's already there); delete both JSON copies | delete | +| `docs/agents/running-and-testing.md` | **merge**: test cmd → AGENTS.md; flags → FlowerBeds.md (already complete there) | delete | +| `docs/agents/python-conventions.md` | **merge**: module table → FlowerBeds.md; rest → `agents/conventions.md` | rename | +| `docs/agents/domain.md` | **delete** (template) | — | +| `docs/agents/triage-labels.md` | **merge** 5 lines → git-workflow.md | delete | +| `docs/agents/issue-tracker.md` | **merge** → git-workflow.md | delete | +| `docs/agents/firmware.md` | keep, rewrite as index of 9 dirs | `docs/agents/firmware.md` | +| `docs/agents/git-workflow.md` | keep, absorb two files | `docs/agents/git-workflow.md` | +| `docs/Dashboard.md` | keep, cut JSON copy, state relay limitation | `docs/Dashboard.md` | +| `docs/FlowerBeds.md` | keep, fix BlobTracker/Stabilizer, drop `/flowerbeds/mode` row | `docs/FlowerBeds.md` | +| `docs/TreeHouse.md` | keep, 4→5, note standalone, link firmware README | `docs/TreeHouse.md` | +| `docs/FundingCAPTCHA.md` | keep, cut to ≤200: maintenance → showtime, private content ≤30, drop spec links, absorb games table | `docs/FundingCAPTCHA.md` | +| `docs/PlayingThePipes.md` | keep, real project names | `docs/PlayingThePipes.md` | +| `docs/PRD.md` | **delete** (optionally 30 lines → README) | — | +| `docs/games/bodycaptcha.md` | **merge** → `games/bodycaptcha.py` module docstring + FundingCAPTCHA.md games table | delete | +| `docs/games/bodygrid.md` | **merge** → `body_grid.py` docstring (already substantial) | delete | +| `docs/games/keepaway.md` | **merge** → `games/body_keepaway.py` docstring | delete | +| `docs/games/rhythm.md`, `upsidedown.md` | **delete**; open one GitHub issue "port UpsideDown/Rhythm to silhouette" if still wanted | — | +| `docs/superpowers/**` (13 files) | **delete**; 5 one-liners → `common.glsl` headers; fix 2 inbound links | — | +| `docs/adr/0007` schema half | **split** → `docs/osc-fabric.md`; ADR keeps rationale | both | +| `docs/adr/0021-fundingcaptcha-shader-player.md` | **renumber** | `0023-…` | +| `docs/adr/0022-shader-player-blob-records.md` | **renumber** | `0024-…` | +| `docs/adr/0004, 0009, 0011` | truncate to header + superseded note (or delete 0009) | same | +| `docs/adr/0010` | status → Superseded by 0020; truncate | same | +| `docs/adr/0015` | cut implementation plan | same | +| other ADRs | add Status, fix listed paths | same | +| `Firmware/TreeHouse_Controllers/README.md`, `ESP32S3_SignsOfLife/README.md` | keep | same | +| `Prototypes/GlowPanels/*.md` (3) | **delete** with the directory, or move the comparison table (README.md L12-24) into a GitHub issue; the Mycelium panel shipped | — | +| `ShowControl/FundingCAPTCHA/{shaders,distribution,videos}/README.md` | keep | same | +| `ShowControl/PlayingThePipes/MakeNoiseDay/other/README.md` | **delete** (third-party) | — | + +### Per-directory README stubs (new) + +A model that opens only this directory should get orientation in ≤20 lines. + +**`IIVision/README.md`** +- One sentence: shared depth-camera CV library; public API is `IIVision/__init__.py` (import from the package, not submodules). +- Coordinate systems: camera X-right/Y-down/Z-forward metres → world X-right/Y-forward/Z-up cm via `orbbec_to_world()`. +- Module map: `camera.py` (Orbbec/Mock), `blob_tracker.py` (per-frame detection + calibration), `blob_stabilizer.py` (cross-frame IDs, `update()` vs `update_tracks()`), `pipeline.py` (wiring), `silhouette_warp.py` (FundingCAPTCHA reprojection), `transforms.py`. +- Consumers: FlowerBeds uses the full pipeline; FundingCAPTCHA uses calibration + subtraction + warp, and `BlobStabilizer` via `body_blobs.py`. +- Tests: `test_*.py` here; run with `python -m pytest IIVision`. + +**`ShowControl/OSCFabric/README.md`** +- `FabricClient(element, network)`: `report(signal, value)` on-change + heartbeat; `send_event(addr)` immediate; `tick(dt)`. +- `load_network_config()` resolves `ShowControl/network.json`; schema is documented in `docs/osc-fabric.md`. +- Only TreeHouse *receives*; this package is the *sender* side. + +**`ShowControl/FundingCAPTCHA/games/README.md`** +- Game interface (`create(settings) -> Game`, `update/draw/reset`, returns `(intensity, event)`). +- `bodycaptcha.py` is the only module loaded (`app.py:_load_games`); `body_keepaway.py` is retained but unloaded. +- `grid.py` = shared overlay/shell (ADR-0019). Input layer is `../body_grid.py`. +- Level data lives beside `app.py` (`bodycaptcha-levels.json`, `taunts.json`). +- Every Game must self-terminate (ADR-0003). + +**`Firmware/README.md`** +- Table of the 9 dirs with status: **live** (`FlowerBeds_Follow_ServoController`, `TreeHouse_Controllers`, `TreeHouse_BranchController`, `PlayingThePipes_EncoderPico`), **diagnostic** (`ESP32S3_SignsOfLife`), **stopgap standalone** (`Club_Pico`, `Fungi_Pico`, `HouseSwarming_Pico` — no host, flash as `main.py`), **superseded** (`TreeHouse_PicoLEDs`, by ADR-0020). +- Which read generated config: only `TreeHouse_Controllers/include/net_config.h`; FlowerBeds `.ino` hardcodes IP/MAC (open item vs AGENTS constraint 3). +- Regenerate config: `python3 scripts/hooks/firmware_config_gen.py`. +- Cross-layer contract: `/cg/ff/rot` ↔ `ShowControl/FlowerBeds/main.py`. + +**`scripts/README.md`** +- `deploy.sh ` — LAN push + install (venue); `bootstrap-deploy.sh` — one-time per Pi; `install-services.sh` — on-Pi installer wrapper. +- `install-git-hooks.sh` + `git-hooks/pre-commit` — private-content guard. +- `hooks/firmware_config_gen.py` — network.json → firmware headers; `hooks/smoke_test.py` — what it checks (one line). +- `test_git_hooks.py` runs under `make test`. + +**`ShowControl/TreeHouse/README.md`** (optional; `docs/TreeHouse.md` covers it) — only if the team wants a pointer: "see `docs/TreeHouse.md`; `displays/base.py` defines `Controllable`/`GardenState`; `osc_server.py` docstring is the inbound OSC contract." + +### Things the cleanup should also fix in code/config (not docs), because the docs can't be made true otherwise + +1. `Makefile`: drop `node --test` (no JS tests since 2026-05-11) or add one; AGENTS.md L8 follows. +2. Either finish the `*Display` → `*Controllable` rename or delete the avoid-notes in CONTEXT.md. +3. Either generate `config.h` for the FlowerBeds `.ino` (the TODO in `firmware_config_gen.py:20`) or amend constraint 3 to say "TreeHouse controllers only, FlowerBeds pending". +4. Remove `ShowControl/TreeHouse/looking_glass/deploy/` (ADR-0018 says it must not be installed). +5. `docs/adr/` renumber (two 0021s). diff --git a/docs/audit/2026-11-09/detail/firmware-prototypes-pipes.md b/docs/audit/2026-11-09/detail/firmware-prototypes-pipes.md new file mode 100644 index 0000000..ef80c77 --- /dev/null +++ b/docs/audit/2026-11-09/detail/firmware-prototypes-pipes.md @@ -0,0 +1,253 @@ +# Audit: Firmware/, Prototypes/, ShowControl/PlayingThePipes/, TouchOSC/ + +Read-only audit of `the repo root` at HEAD `56de83c` (2026-11-08). Full history: 207 commits, 2026-01-24 → 2026-11-08. All paths below are relative to the repo root unless absolute. + +Headline numbers (git-tracked, `du -k` blocks): + +| Area | Files | Lines | Tracked KB | LFS objects behind pointers | +|---|---|---|---|---| +| Whole repo | 1 pack, 989 objects | — | 10,644 KB | 82 files, 230 MB declared | +| `ShowControl/PlayingThePipes/` | 181 | 163,521 | 8,000 KB (75% of repo) | 57 files, 226 MB (98% of LFS) | +| ↳ `Pipes Prototype 3/` | 110 | 133,349 | 6,832 KB (64% of repo) | 24 files, 112 MB | +| ↳ `MakeNoiseDay/` | 26 | 16,557 | 576 KB | 0 | +| ↳ `Lamplight2026/` | 42 | 11,804 | 660 KB | 33 files, 114 MB | +| `Firmware/` (9 dirs) | 46 | 5,117 (3,357 of which TreeHouse_Controllers code; 582 of which tracked `.vscode` junk) | 328 KB | 0 | +| `Prototypes/` (3 dirs) | 10 | 1,942 | 96 KB | 0 | +| `TouchOSC/` | 1 | (binary) | 4 KB | 0 | + +--- + +## 1. Firmware inventory + +Per-directory facts. "Refs" = anything outside the directory itself that names it (grep across repo, excluding `.git` and `__pycache__`). + +| Dir | MCU / toolchain | Drives | Code LOC | Commits (first → last) | Refs outside dir | README | Status | +|---|---|---|---|---|---|---|---| +| `FlowerBeds_Follow_ServoController/` | OpenRB-150 (SAMD21), Arduino `.ino`; libs Dynamixel2Arduino, Ethernet, OSCMessage | Dynamixel servos via Ethernet+OSC `/cg/ff/rot`, `/cg/ff/ping|pong|hb` | 207 | 4 (2026-02-05 → 2026-05-25) | `AGENTS.md`, `docs/agents/firmware.md`, `docs/agents/architecture.md:16`, `ShowControl/FlowerBeds/diag.py:4`, `scripts/hooks/firmware_config_gen.py:19` | **no** | **LIVE** | +| `TreeHouse_BranchController/` | OpenRB-150 (SAMD21), Arduino `.ino`; libs Dynamixel2Arduino, ArduinoJson | Branch Dynamixels via USB JSON-lines (ADR-0005) | 132 | 1 (2026-05-03) | `docs/agents/firmware.md:17`, `architecture.md:17`; counterpart `ShowControl/TreeHouse/branch_controller.py` | **no** | **LIVE** | +| `PlayingThePipes_EncoderPico/` | RP2040 Pico, MicroPython | 6 quadrature encoders → `/enc BOARD ENC DELTA` over USB CDC (ADR-0014) | 52 | 1 (2026-05-22) | `docs/PlayingThePipes.md:18`; consumed by `Lamplight2026/patchers/encoderin.maxpat:165,231` (`serial #1`, `serial #2`) and `EncoderTester.maxpat:1415` | **no** | **LIVE** | +| `TreeHouse_Controllers/` | ESP32-S3 DevKitC-1 (4 MB), PlatformIO/Arduino C++17, NeoPixelBus; `env:native` for Unity tests | 5 locations: SK6812 strips, PWM MOSFET dimmers, cap-discharge arc trigger (ADR-0020/0021) | 3,357 (src 807, lib 1,031, test 1,113, ini 112 …) | 3 (2026-09-06 → 2026-10-27) | `docs/agents/firmware.md`, `docs/TreeHouse.md`, ADR-0020/0021, `scripts/hooks/firmware_config_gen.py`, `ESP32S3_SignsOfLife/README.md` | **yes** (174 lines, good) | **LIVE** | +| `ESP32S3_SignsOfLife/` | ESP32-S3, PlatformIO/Arduino, Adafruit NeoPixel | Board bring-up: 8-px strip RGBW cycle + onboard LED + serial heartbeat + `esp_reset_reason()` | 125 (88 `main.cpp` + 37 ini) | 1 (2026-09-06) | `TreeHouse_Controllers/README.md:58-60` only | yes (101 lines) | **LIVE diagnostic**, but a hello-world | +| `TreeHouse_PicoLEDs/` | RP2040 Pico, MicroPython + PIO | SK6812 pixel sink over USB JSON lines (ADR-0010) | 64 | 1 (2026-05-07) | `docs/agents/firmware.md:48` ("superseded"), ADR-0010, ADR-0020, `architecture.md:18`, docstrings in `Club_Pico/main.py:18`, `HouseSwarming_Pico/main.py:18` | **no** | **SUPERSEDED (ADR-0020) but still wired on the Pi** — see below | +| `HouseSwarming_Pico/` | Pico, MicroPython + PIO | 3 standalone SK6812 strips: fireplace flicker + 2 static warm-white overheads, no host | 106 | 1 (2026-05-29 "stopgap firmware for house lights") | **zero** | **no** | **DEAD** — functionally replaced by `TreeHouse_Controllers/src/targets/swannatopia.h` (fire + incandescent overhead + chandelier) | +| `Club_Pico/` | Pico, MicroPython + PIO | 2 standalone SK6812 strips (magenta/cyan bands, cool-white comet) + PWM superbright LED, no host | 132 | 1 (2026-05-29) | **zero** | **no** | **DEAD** — replaced by `targets/jess.h` (2 `Rave` strips + `Flash` MOSFET) | +| `Fungi_Pico/` | Pico, MicroPython | 1 PWM filament gamma-corrected sine breathe 30–80 %, 16 s, no host | 45 | 3 (2026-05-29 → 2026-05-30 "fix fungi/dormer") | **zero** | **no** | **DEAD** — replaced by `targets/julia.h` (`Wander` 40–80 %) and `targets/dormer.h` (`Breathe` 12.5 s 40–80 %) | + +Also deleted from history already: `Firmware/Dynamixel_Config/` (removed 2026-04-04, `7581a4b`). No `Firmware/README.md` at the top level, so a reader has no map of these nine directories. + +### 1a. The three "stopgap" Picos are dead code + +`Firmware/Club_Pico/main.py:1`, `Firmware/HouseSwarming_Pico/main.py:1`, `Firmware/Fungi_Pico/main.py:1` each open with `"... standalone Pico light controller (stopgap)"`. They were a one-day fix (2026-05-29/30) for the TreeHouse house lights before any host-driven path existed. Each duplicates the same 15-line PIO SK6812 driver (`_sk6812`, `Strip`) copy-pasted from `TreeHouse_PicoLEDs/main.py:8-22`. Nothing in `ShowControl/`, `docs/`, `scripts/` or any ADR mentions them. The ESP32-S3 project (ADR-0020, amended ADR-0021 to run standalone — i.e. exactly the "stopgap" behaviour, but on the new hardware) covers every one of their roles, location by location. **Delete all three (283 LOC, 3 files).** + +### 1b. `TreeHouse_PicoLEDs` — superseded on paper, still deployed in code + +ADR-0020 `Consequences`: "`Firmware/TreeHouse_PicoLEDs/` is superseded. It stays in the tree until the ESP32-S3 hardware is physically installed, then is removed along with `pico_driver.py` and the `pico_*` blocks in `settings.json`." None of that removal has happened: + +- `ShowControl/TreeHouse/main.py:24` `from pico_driver import PicoDriver`; `:149` `driver = PicoDriver(config.pico.port, config.pico.baud)`; `:150` connects unless `--no-pico`. +- `ShowControl/TreeHouse/deploy/treehouse.service:14` `ExecStart=/usr/bin/python3 main.py --config settings.json` — no `--no-pico`, so production still opens the Pico. +- `ShowControl/TreeHouse/settings.json:2,7` still has `pico_dioramas` / `pico_structure` blocks (ADR-0010 names), while `main.py:149` reads a single `config.pico` — flag to the ShowControl auditor; it looks like the two-Pico split from ADR-0010 was never finished in `main.py`. +- `docs/TreeHouse.md:15` (last edited 2026-09-06) says the Picos are "still wired until the ESP32-S3 hardware is installed". + +Whether the ESP32 boards are physically in the house is not knowable from the repo (`garage.h:22` still says pins are placeholders; ADR-0021 is dated 2026-10-27 and speaks of "this round of the show"). **Verdict: keep until the owner confirms hardware; then delete `TreeHouse_PicoLEDs/`, `pico_driver.py`, the `pico_*` settings, `--no-pico`, and the udev section of `docs/TreeHouse.md:164-171` in one PR.** Note ADR-0010 says udev rules are "generated by `scripts/install-services.sh`" — `grep -n "pico\|udev\|treehouse-" scripts/install-services.sh` returns nothing. That claim was never true in main. + +### 1c. `ESP32S3_SignsOfLife` is a hello-world — a deliberate, documented one + +`src/main.cpp:1-6`: "the smallest thing that proves the board boots … No WiFi, no OSC, no Garden State, no config headers." Its README is a genuinely useful boot-loop/brownout triage guide (`README.md:55-87`) and is cross-referenced from `TreeHouse_Controllers/README.md:58`. But it duplicates the entire ESP32-S3 `platformio.ini` board block (`ESP32S3_SignsOfLife/platformio.ini:16-29` ≡ `TreeHouse_Controllers/platformio.ini:20-32`, same 4 MB flash workaround comment verbatim) plus a second `.vscode/` and `.gitignore`. **Verdict: fold it into `TreeHouse_Controllers` as `[env:signsoflife]` with `build_src_filter = +` (or similar) and move the triage README into a "Bring-up" section of the main README; delete the directory (5 files → 1 .cpp).** One PlatformIO project, one `.vscode`, one place to look. + +### 1d. `firmware_config_gen.py` — generates one header, the docs claim two + +`scripts/hooks/firmware_config_gen.py:14-19`: + +``` +Currently generated: + Firmware/TreeHouse_Controllers/include/net_config.h (ADR-0020) +TODO: generate Firmware/FlowerBeds_Follow_ServoController/config.h too. +``` + +Only `TREEHOUSE_HEADER` is written (`:26-28`, `generate()` `:90-94`). Consumers: `TreeHouse_Controllers/src/targets/target.h:7` and `src/Net.cpp:6` `#include "net_config.h"`. The committed `include/net_config.h` matches `ShowControl/network.json` (`firmware.treehouse_*` 192.168.1.60-64, port 9000; `lan` block) — it is a generated-but-committed file, by design. + +The FlowerBeds sketch does **not** consume any generated header. `FlowerBeds_Follow_ServoController.ino:25-27` hardcodes: + +``` +byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x15, 0x01 }; +IPAddress ip(192, 168, 1, 51); // set for your LAN +const uint16_t localPort = 9000; +``` + +plus `HB_PORT = 9100` at `:35`. That is `network.json` `firmware.flowerbeds_controller_2` typed by hand, and a direct violation of AGENTS.md constraint #3. Both `docs/agents/firmware.md:8` ("IP and MAC come from a generated `config.h`") and `docs/agents/configuration.md:113` ("Firmware reads addresses from a generated `config.h`") describe a mechanism that does not exist. Either finish the TODO (render `config.h` for both `flowerbeds_controller_1/2`, `#include "config.h"` in the sketch, gitignore it) or fix the docs. The first is ~20 lines of Python and makes the second-board MAC/IP collision risk (`firmware.md:10`) go away. + +### 1e. `TreeHouse_Controllers/lib/*` and `test/` + +All five libs are used (`#include` graph): `Patterns` ← `GardenState`, `Rng`, `Weld`; `GardenState` ← `OscRx`; `Weld` ← `Rng`; `src/` includes `GardenState`, `OscRx`, `Patterns`, `Outputs`, `Net`. `lib/` contains only ` ` — the "Arduino-free" claim in `firmware.md:28` holds. No `library.json` manifests (PlatformIO infers; fine). + +Dead code inside `lib/Patterns`: `PatternId` has 11 values (`Patterns.h:33-48`); targets use 7 (`Breathe, Fire, Flash, Incandescent, Rave×2, Wander, Weld`). `Solid`, `Chase`, `Filament` are referenced only by tests; **`Mycelium` (`Patterns.h:37`, `Patterns.cpp:255`) is referenced by no target, no test, no self-test** — it is the old Pico-era diorama pattern carried over. Candidate for removal with the other three if the owner agrees they are not planned. + +Tests (`pio test -e native`): 51 cases across 4 suites — `test_garden_state` (7: Fabric address folding, clamping, one-shot blow-up, mode strings, staleness), `test_oscrx` (8: float/string/int/argless parsing, malformed packets, bundles, lying bundle sizes), `test_patterns` (30: signal bag, dim/inactive, stale breathe, blow-up, flash, chase, fire, breathe, wander, weld, rave, gamma), `test_weld` (6: session/burst/tap gaps, recharge guarantee, seeding, millis rollover). Coverage of the host-testable logic is good. **They are not run in CI**: `.github/workflows/tests.yml` installs Node + Python only; `Makefile` runs `node --test` and `pytest`; no `pio` anywhere outside `firmware.md:28`. + +Tracked junk: `Firmware/TreeHouse_Controllers/.vscode/c_cpp_properties.json` (538 lines, 68 KB, **483 lines containing `/home/charliehuge/...`**) and `.vscode/launch.json` (44 lines, absolute `/home/charliehuge/projects/...` paths, header says "AUTOMATICALLY GENERATED FILE"). Both are listed in the project's own `.gitignore:3-5` and `git ls-files -i -c --exclude-standard` confirms they are tracked anyway (added in `67b2098`, re-touched in `2ce5f20`). `git rm --cached` them. The 24th-largest file in the whole repo is a PlatformIO IntelliSense cache. + +### 1f. READMEs + +Only 2 of 9 firmware dirs have one. `FlowerBeds_Follow_ServoController`, `TreeHouse_BranchController`, `PlayingThePipes_EncoderPico` are live production firmware whose required libraries and flashing procedure live only in `docs/agents/firmware.md` / `docs/PlayingThePipes.md`. Each needs a 10-line README (board, libs, what it listens to, how to flash) or the directory is not self-describing. + +--- + +## 2. Prototypes + +| Dir | What | Files / lines | Commits | Code shared with ShowControl? | Verdict | +|---|---|---|---|---|---| +| `ForgeAndFlora/forge_and_flora.py` | Pico MicroPython bench tester for the Forge & Flora diorama: 6-px "forge" strip (amber flicker + blue-white arc flash) and 4-px "flora" strip (green/yellow breathe) | 1 / 121 | 1 (2026-05-07 "add fnf tester firmware") | **No.** `grep -rlw` of its symbols (`write_pixel`, `update_forge`, `update_flora`) in `ShowControl/`+`IIVision/` → 0 hits. | **DELETE** | +| `GlowPanels/` | Fabrication notes for two acrylic "mycelium glow panel" construction methods (side-lit vs back-lit frosted) + a 110-LOC generic breathing/gradient Pico script | 4 / 474 (3 READMEs = 364 lines) | 2 (2026-04-20, 2026-05-07 "update panel prototype firmware with LED counts for Mycelium") | **No code.** But `firmware/panel_gradient.py:24` `NUM_LEDS = 224` matches `ShowControl/TreeHouse/settings.json:49` Mycelium `led_count: 224` — this is the build log for the Mycelium diorama that now ships in production. | **MOVE the two build READMEs** to `docs/` (fabrication record), **delete** `panel_gradient.py` and the top README | +| `TouchTest/` | Orbbec Gemini 336L depth-touch experiments for FundingCAPTCHA: `touch_test.py` (815 lines, pygame calibrator: BG model → 4-corner perspective cal → live grid), `captcha_touch.py` (417, 4×4 Schmitt-trigger grid detector emitting OSC `/captcha/touch/*`), `ir_test.py` (102, IR viewer) | 5 / 1,347 | 1 (2026-05-10 "Add camera touch calibrator (#79)") | **No.** 14 distinctive symbols (`BgCal`, `CornerCal`, `GridTouchDetector`, `ENTER_MM`, `FRAMES_TO_ENTER`, `_corner_contact_point`, …) → 0 hits in `ShowControl/`+`IIVision/`. Production FundingCAPTCHA uses `body_blobs.py`/`body_grid.py`/`silhouette.py` (ADR-0013) and the Fabric schema (ADR-0007) has no `/captcha/touch/*`. | **DELETE** | + +`ShowControl/TreeHouse/test_forge_and_flora.py` tests the **production** module: `:5` `from displays.effect import ForgeAndFloraConfig, ForgeAndFloraDisplay` (`ShowControl/TreeHouse/displays/effect.py:11,22`). It has nothing to do with `Prototypes/ForgeAndFlora/`. Note the irony: the Pi-side `ForgeAndFloraDisplay` is itself on the chopping block once the Pico path goes (section 1b), and the ESP32 `Weld` pattern (`lib/Patterns`) is the third-generation implementation of the same idea. + +`pytest.ini` `testpaths = ShowControl IIVision scripts` — nothing under `Prototypes/` is collected or tested. No doc anywhere (`docs/`, `README.md`, `CONTEXT.md`, `AGENTS.md`) mentions `Prototypes/`. `Prototypes/FundingCAPTCHA/` was already deleted on 2026-04-30 (`3dd3d06`), so there is precedent for retiring a prototype once it graduates. + +Whole directory: 10 files, 1,942 lines, 96 KB → 0 files if the GlowPanels READMEs move to `docs/`. + +--- + +## 3. PlayingThePipes + +### 3a. Which patch is the show? + +**`Lamplight2026/patchers/Lamplight2026.maxpat`** is the current show patch — by elimination and evidence: + +- Newest: `a25d6a5` 2026-10-27 "Ptp ll26 (#155)" touched only `Lamplight2026/`; it was created in `4b72fbf` 2026-10-01 "Ptp v3 (#153)" (38 files added). +- It is the only project that contains the encoder-Pico serial reader: `Lamplight2026/patchers/encoderin.maxpat:165` `"serial #2"`, `:231` `"serial #1"` (two Picos, matching ADR-0014), and `fake_encoder.maxpat` for bench use. +- `Lamplight2026.maxproj` is the only project with a dependency on another project (see 3c). + +**`docs/PlayingThePipes.md` does not say so.** It names `ShowControl\PlayingThePipes\PlayingThePipes.maxpat` at `:11`, `:43`, `:93`, as does `docs/bootstrap.md:173,198`. That file has **never existed on `main`**. The only place it ever existed is `ShowControl/PlayingThePipes/Music/PlayingThePipes.maxpat` on the unmerged branch `origin/claude/max-music-system-stub-z2Qs0` (`f486680`, 2026-04-17, "Add Playing The Pipes music system Max patch stubs", 8 stub patchers). `docs/PlayingThePipes.md` was written once (2026-05-25, `0b9f044`) against that stub and never updated through three rounds of real patches. + +Other doc claims vs patch reality: "Max/RNBO" (`PlayingThePipes.md:11`, `README.md:23`, ADR-0014 "RNBO patch") — `grep -rl 'rnbo~'` across all `.maxpat` → **0 files**. The engine is `grainflow~` (third-party Grainflow package), `groove~`, `mc.*~`, and two Ableton device objects `abl.device.reverb~`, `abl.device.spectraltime~` (Max 9 `abl.*` package). None of these dependencies is documented anywhere. "`/pipes/activity` is planned but not wired" — true: no `udpsend` in any patch. + +### 3b. The three projects + +| Project | Files / lines / tracked KB | LFS media | Commits | Reachable from its top patch | Verdict | +|---|---|---|---|---|---| +| `Lamplight2026/` | 42 / 11,804 / 660 KB | 33 files, 114 MB | 2 (2026-10-01, 10-27) | 6 local patchers (`encoderin`, `fake_encoder`, `isactive`, `knobgrain`, `knobvelocity`, `midilearn`) + 2 from MakeNoiseDay | **KEEP** (current show) | +| `MakeNoiseDay/` | 26 / 16,557 / 576 KB | 0 | 3 (2026-09-09, 09-25 "Pipes 0coast", 10-01) | top patch `MakeNoiseDay.maxpat` uses 5 of its 15 patchers + 0 of 2 `.gendsp`; **Lamplight2026 uses exactly 2: `scaleclip.maxpat` (169 lines) and `wakemove.maxpat` (210 lines)**, both via `knobgrain.maxpat` | **Copy those 2 files into `Lamplight2026/patchers/`, then DELETE** (24 files, 16,178 lines) | +| `Pipes Prototype 3/` | 110 / 133,349 / 6,832 KB | 24 files, 112 MB | 1 (2026-09-09 "carolina tweaks (#141)", 110 files added at once) | top patch uses 14 of 62 patchers; **Lamplight2026 references nothing in it** (transitive closure over `text` and bpatcher `name` fields) | **Tag `archive/pipes-prototype-3`, DELETE from main** | +| `EncoderTester.maxpat` | 1 / 1,754 / 72 KB | — | 1 (2026-05-22) | — ; has `serial h` at `:1415`, `/enc` parsing | KEEP (referenced by `PlayingThePipes.md:47,92`) | +| `health_server.py`, `requirements.txt` | 2 / 57 | — | — | — | KEEP (NSSM service, `bootstrap.md:185-191`) | + +`MakeNoiseDay/other/README.md` + `other/LICENCE` identify it as a personal modular-synth Max library (MIT 2017, with streaming links) dropped into the repo wholesale; it is a vendored dependency, not show code. `Lamplight2026.maxproj:20-40` pins its three MakeNoiseDay dependencies with `"bootpath": "~/Projects/CommunityGarden/ShowControl/PlayingThePipes/MakeNoiseDay/patchers"` — a machine-specific absolute path, and one of the three (`autograin.maxpat`) is referenced by **no** Lamplight2026 patcher (stale `.maxproj` entry). Copying `scaleclip`/`wakemove` in and dropping the singleton entries makes Lamplight2026 self-contained. + +### 3c. Duplicates inside `Pipes Prototype 3/patchers/` + +The ten `X.amxd` / `X.maxpat` pairs with identical line counts (Dual Harmonizer 6,775; Fragulator 910; Max AutoFilter 2,086; Max AutoRingMod 1,373; Max CombFilterMulti 1,494; Max EqShelving 2,312; Max SpectralHarm 3,875; Noyzckippr 3,721; Phone Filter 1,547; Speed Shifter 1,643) are **the same bytes plus a 20-byte Max-for-Live `ampf…ptch` wrapper**: `cmp` differs, `diff` reports 0 differing lines, and Python confirms `amxd[20:] == maxpat` exactly (e.g. `Fragulator.amxd` 35,787 B vs `.maxpat` 35,767 B). Both halves are binary-framed (`mx@c` header, `flag`/`mdat` trailer) exported Ableton devices, not hand-written patchers. That is 1,744 KB of `.amxd` duplicated as 1,744 KB of `.maxpat` — 33 % of the repo's tracked bytes is one copy of ten Live devices, and another 16 % is the second copy. `Pendulum.amxd` (152 KB) has no twin. + +Also in P3: `fragment`/`fragmentOLD`, `pendulum`/`pendulumOLD`, `spectralharm`/`spectralharmOLD` (near-identical: 50/101/92 differing lines), nine `data/*.amxd_20260902.maxsnap` snapshot copies beside their `*.amxd.maxsnap` originals (4–24 line deltas), and 36 patchers reachable from nothing (full list in the appendix). `Lamplight2026/patchers/knobgrain.maxpat` shares 751 sorted lines with P3's `bp.rgrain.maxpat` — the useful part was already ported. + +### 3d. Media + +All 57 `.wav`/`.png` under `PlayingThePipes/` are **Git LFS pointer files** (`.gitattributes` tracks `*.wav *.png *.jpg *.mp3 …`), 3 lines each; the objects are not even fetched in this clone (`git lfs ls-files` shows `-` for every one). Declared sizes: + +- `Lamplight2026/media/`: 33 files, **114.3 MB**. Referenced by name from Lamplight2026 patchers: 13 files, 73 MB. **Unreferenced by name: 20 files, 41 MB**, including `tone.04.wav` alone at 22.3 MB, `scrapes.wav` 5.6 MB, `waterBell1.wav` 4.2 MB, two `.png` logos/banners (0.7 MB). Caveat: Max can load buffers by message at runtime, so "unreferenced by static grep" needs a 5-minute confirmation in Max before deletion. `grainflow.wav` is referenced (`Lamplight2026.maxpat`) but not present in `media/` — either a Grainflow-package sample or a missing asset. +- `Pipes Prototype 3/media/`: 24 files, **112.0 MB — every one byte-identical to its Lamplight2026 twin** (`cmp` on all 24 pointer files). Pure duplication. + +Is 226 MB of samples appropriate here? LFS keeps them out of the pack (`.git` is 2.6 MB), so clone cost is fine, but it costs LFS storage/bandwidth quota and every `git lfs pull` on a show machine drags 112 MB of dead duplicates. The deployment path (`PlayingThePipes.md:71-74`: `git push … pipes:CommunityGarden` to the Windows host) does need the samples to travel with the patch, which argues for keeping the **referenced** Lamplight2026 samples in LFS (or in the private assets repo, mirroring how `ShowControl/FundingCAPTCHA/images/private/` is handled — see AGENTS.md #6). Either way: delete the P3 copies (−112 MB LFS), confirm-then-delete the 20 unreferenced Lamplight files (−41 MB), leaving ~73 MB that is actually played. + +### 3e. Verdict per project + +1. **Lamplight2026** — keep in repo; make self-contained (copy 2 patchers in, fix `.maxproj` bootpaths); add a README naming it as the show patch and listing externals (Grainflow, `abl.*`, two serial COM ports); prune unreferenced media after confirming in Max. +2. **MakeNoiseDay** — delete after the 2-file copy (−24 files, −16,178 lines, −560 KB). If the author wants it preserved, it belongs in its own repo (it already has its own README/LICENCE). +3. **Pipes Prototype 3** — `git tag archive/pipes-prototype-3 ` then delete (−110 files, −133,349 lines, −6.8 MB tracked, −112 MB LFS). It is 82 % of the repo's lines and nothing live depends on it. + +--- + +## 4. TouchOSC + +`TouchOSC/FlowerBedTester.tosc` — 4 KB zlib-compressed TouchOSC-mk2 layout (16.5 KB XML when inflated). Contents: one GROUP with 4 fader-style controls, each OSC-enabled and named `cg/ff/rot`, sending `/` with a constant INTEGER argument (motor id) + the control value — i.e. a hand controller for `/cg/ff/rot [int motor_id, float deg]`, matching `FlowerBeds_Follow_ServoController.ino:82,193-199`. 3 commits (2026-02-16 → 2026-04-17), none since. Referenced by `docs/agents/running-and-testing.md:69` ("validate with `TouchOSC/FlowerBedTester.tosc`") and `architecture.md:19`. ADR-0005:23 and ADR-0007:27,91 mention TouchOSC as a possible operator surface for TreeHouse; no such layout exists. + +Verdict: **LIVE but opaque.** Keep, but it should not be a top-level directory for one 4 KB binary. Move to `ShowControl/FlowerBeds/tools/FlowerBedTester.tosc` (next to `diag.py`, which does the same job from Python) with a 5-line README (what it sends, which host/port to set in the app — `network.json` `firmware.flowerbeds_controller_*`). Alternatively, if `diag.py` has made it redundant, delete it and fix `running-and-testing.md:69`. + +--- + +## 5. Repo-size hygiene + +Tracked bytes by extension (`du -k` over `git ls-files`, basename-safe): + +| Ext | KB | Files | Note | +|---|---|---|---| +| `.maxpat` | 6,012 | 78 | 5,500+ KB of it under `Pipes Prototype 3/` | +| `.amxd` | 1,744 | 11 | all `Pipes Prototype 3/`; 10 are twins of `.maxpat` files | +| `.py` | 908 | 92 | | +| `.md` | 556 | 66 | | +| `.glsl` | 248 | 24 | | +| `.wav` (LFS pointers) | 212 | 53 | 226 MB behind them | +| `.json` | 196 | 23 | 68 KB is `c_cpp_properties.json` | +| `.maxsnap` | 92 | 23 | all P3; 9 are dated duplicates | +| everything else | < 700 | | | + +Top 20 largest tracked files: **all 20 are under `ShowControl/PlayingThePipes/`** (19 in `Pipes Prototype 3/patchers/`, 1 is `Lamplight2026.maxpat` at 408 KB; `MakeNoiseDay.maxpat` 212 KB is #5). #1 is `Pipes Prototype 3/patchers/Pipes Prototype 3.maxpat` at 2,196 KB / 45,901 lines — 21 % of the repo by bytes, 28 % by lines. `Firmware/TreeHouse_Controllers/.vscode/c_cpp_properties.json` (68 KB) is #24. + +Should be untracked / gitignored: + +- `Firmware/TreeHouse_Controllers/.vscode/c_cpp_properties.json`, `.vscode/launch.json` — already matched by that project's `.gitignore`, tracked anyway (`git ls-files -i -c --exclude-standard`). `git rm --cached`. +- Root `.gitignore` has no `.pio/` or PlatformIO `.vscode` entries; it relies on per-project `.gitignore`s (`TreeHouse_Controllers/.gitignore`, `ESP32S3_SignsOfLife/.gitignore`). Add `**/.pio/`, `**/.vscode/c_cpp_properties.json`, `**/.vscode/launch.json`, `**/.vscode/ipch` at the root so the next PlatformIO project cannot repeat this. +- `Prototypes/TouchTest/.gitignore` (`calibration.npz`) goes away with the directory. +- `*.maxsnap` dated snapshots (`*_20260902.maxsnap`) are editor state, not source; gone with P3. + +Not a problem: `.git` pack is 1.77 MB; no `.pyc` tracked; `ShowControl/FundingCAPTCHA/deploy/private-assets.example.env` is an example file, not a secret. `include/net_config.h` is generated-and-committed by design. + +--- + +## 6. Docs drift + +| Doc | Line(s) | Says | Reality | +|---|---|---|---| +| `docs/PlayingThePipes.md` (1 commit, 2026-05-25) | 11, 43, 93 | Show patch is `PlayingThePipes.maxpat`; "Max/RNBO" | File never existed on `main` (only on unmerged `origin/claude/max-music-system-stub-z2Qs0`). Show patch is `Lamplight2026/patchers/Lamplight2026.maxpat`. No `rnbo~` in any patch; engine is `grainflow~`/`groove~`/`abl.device.*`. | +| `docs/PlayingThePipes.md` | 50-57 | COM port table "TBD" | Still TBD five months later; `encoderin.maxpat` uses `serial #1`/`#2` arguments, so COM names are patcher args, not hardcoded — doc should say that. | +| `docs/bootstrap.md` (2026-09-06) | 16, 173, 198 | "Max/RNBO audio I/O"; "Record COM numbers in `PlayingThePipes.maxpat`"; "Open `…\PlayingThePipes.maxpat`" | Same non-existent file. | +| `README.md` | 23 | "Max/RNBO music system" | No RNBO. | +| `docs/agents/firmware.md` (2026-10-27) | 8 | FlowerBeds "IP and MAC come from a generated `config.h`" | `.ino:25-27` hardcodes them; generator has it as `TODO` (`firmware_config_gen.py:19`). | +| `docs/agents/firmware.md` | whole file | Lists 4 firmware dirs | 9 exist; `Club_Pico`, `HouseSwarming_Pico`, `Fungi_Pico`, `ESP32S3_SignsOfLife`, `PlayingThePipes_EncoderPico` are absent. | +| `docs/agents/configuration.md` (2026-05-22) | 113 | "Firmware reads addresses from a generated `config.h`" | Only `net_config.h` for TreeHouse_Controllers is generated. | +| `docs/agents/architecture.md` (2026-05-22) | 15-19 | Tree lists 3 firmware dirs, `TouchOSC/`, no `Prototypes/` | 9 firmware dirs, `Prototypes/` exists with 3 subdirs. | +| `docs/agents/architecture.md` | 26 | "`pytest.ini` — testpaths = ShowControl IIVision" | `pytest.ini` says `ShowControl IIVision scripts`. | +| `docs/TreeHouse.md` (2026-09-06) | 14, 196 | "4× ESP32-S3 … Swannatopia, Julia, Jess, Dormer" / "Four ESP32-S3 controllers" | Five since ADR-0021 (Garage, 192.168.1.64); `settings.json` `locations.controllers` lists 5; `net_config.h:29-32` has `GARAGE`. | +| `docs/TreeHouse.md` | 15-17, 164-171, 234 | Pico A/B wired, udev symlinks, "LEDs dark → check `/dev/treehouse-pico-a`" | Consistent with code (`main.py:149`) but contradicts ADR-0020's stated removal; the repo cannot tell which is true on site. | +| ADR-0010 | header | `Status: Accepted` | Superseded by ADR-0020 (which says so in *its* header); ADR-0010 should carry `Superseded by ADR-0020`. | +| ADR-0010 | "Stable device names via udev" | "Udev rules are generated by `scripts/install-services.sh`" | No udev/pico content in that script, in any revision on `main`. | +| ADR-0014 | Decision | "routes events into the RNBO patch" | No RNBO. Protocol (`/enc BOARD ENC DELTA`) and pin map match `PlayingThePipes_EncoderPico/main.py:11-48` exactly — the rest is accurate. | +| ADR-0020 | Consequences | PicoLEDs/`pico_driver.py`/`pico_*` removed when hardware installed; `location_sender.py` "additive" | Nothing removed; both paths still run (section 1b). Also says "four environments" — amended by ADR-0021 (header notes this; fine). | +| ADR-0021 | all | `kFollowGardenState` in `src/main.cpp`; Garage pins placeholders; `treehouse_garage` 192.168.1.64 | All verified: `main.cpp:48`, `garage.h:22`, `network.json`, `net_config.h:29-32`. **Accurate.** | +| ADR-0005 | all | USB JSON-lines, same protocol as `pico_driver.py` | Matches `TreeHouse_BranchController.ino:5-11`. **Accurate.** | +| `Firmware/TreeHouse_Controllers/README.md` | 164 | "Stop sending for 10 s and the channels drop to idle breathe" | Only when `kFollowGardenState == true`; the README does qualify this at :151, but :164 reads as unconditional. Minor. | + +--- + +## Ranked recommendations + +1. **Delete `ShowControl/PlayingThePipes/Pipes Prototype 3/`** after tagging `archive/pipes-prototype-3`. −110 files, −133k lines, −6.8 MB tracked (64 % of repo), −112 MB LFS. Nothing live references it. +2. **Collapse `MakeNoiseDay/` into `Lamplight2026/`**: copy `scaleclip.maxpat` + `wakemove.maxpat` into `Lamplight2026/patchers/`, drop the three `singleton.bootpath` entries from `Lamplight2026.maxproj`, delete `MakeNoiseDay/`. −24 files, −16k lines. Then write `ShowControl/PlayingThePipes/README.md` naming Lamplight2026 as the show patch and listing externals. +3. **Fix `docs/PlayingThePipes.md`, `docs/bootstrap.md`, `README.md`**: replace every `PlayingThePipes.maxpat` with `Lamplight2026/patchers/Lamplight2026.maxpat`; drop "RNBO". These docs currently send an operator to a file that has never existed. +4. **Delete `Firmware/Club_Pico`, `HouseSwarming_Pico`, `Fungi_Pico`** (−283 LOC, zero references, superseded by ESP32 targets) and **`Prototypes/ForgeAndFlora`, `Prototypes/TouchTest`** (−1,468 lines, zero shared code). Move the two GlowPanels build READMEs to `docs/`, delete the rest of `Prototypes/`. +5. **`git rm --cached Firmware/TreeHouse_Controllers/.vscode/{c_cpp_properties,launch}.json`** and add PlatformIO patterns to the root `.gitignore`. 483 lines of another machine's home directory are in the repo. +6. **Finish `firmware_config_gen.py`'s TODO** so `FlowerBeds_Follow_ServoController` includes a generated `config.h` instead of hardcoding MAC/IP (`.ino:25-27`, constraint #3 violation) — or correct `firmware.md:8` and `configuration.md:113`. +7. **Fold `ESP32S3_SignsOfLife` into `TreeHouse_Controllers`** as a `signsoflife` environment; one PlatformIO project, one triage README. +8. **Resolve the Pico question on site**: if the ESP32 boards are installed, execute ADR-0020's removal list (`TreeHouse_PicoLEDs/`, `pico_driver.py`, `pico_*` settings, `--no-pico`, `docs/TreeHouse.md:15-17,164-171`); if not, update `docs/TreeHouse.md:14,196` to five controllers and leave the rest. Mark ADR-0010 `Superseded by ADR-0020`. +9. **Prune `Lamplight2026/media/`**: confirm in Max, then delete the 20 files (41 MB LFS) no patcher names, starting with `tone.04.wav` (22 MB). Decide whether the remaining ~73 MB lives in LFS or the private assets repo. +10. **Add `Firmware/README.md`** (one table: dir → board → what it drives → live/retired) and 10-line READMEs for `FlowerBeds_Follow_ServoController`, `TreeHouse_BranchController`, `PlayingThePipes_EncoderPico`. Add `pio test -e native` to CI or state in `firmware.md` that it is manual. +11. Move `TouchOSC/FlowerBedTester.tosc` to `ShowControl/FlowerBeds/tools/` with a README, or delete it in favour of `diag.py`. Remove `PatternId::Mycelium` (and `Solid`/`Chase`/`Filament` if unplanned) from `lib/Patterns`. + +Net effect of 1–5 alone: roughly **−149 files, −152k lines, −7.5 MB of 10.6 MB tracked (≈71 %), −112 MB LFS**, with every surviving directory having a README that says what it is. + +--- + +## Appendix A — `Pipes Prototype 3/patchers/` files reachable from nothing + +`Dual Harmonizer.maxpat`, `Fragulator.maxpat`, `M4L.bal2~.maxpat`, `M4L.comb2.voice~.maxpat`, `M4L.envfol~.maxpat`, `M4L.pan2~.maxpat`, `M4L.transp.pfft~.maxpat`, `M4L.vdelay~.maxpat`, `Max AutoFilter.maxpat`, `Max AutoRingMod.maxpat`, `Max CombFilterMulti.maxpat`, `Max EqShelving.maxpat`, `Max SpectralHarm.maxpat`, `Noyzckippr.maxpat`, `Phone Filter.maxpat`, `Speed Shifter.maxpat`, `autofilter.maxpat`, `bloom.maxpat`, `bloomplay.maxpat`, `bp.rgrain.maxpat`, `cctoplay2.maxpat`, `frag.stutter_Pshift22~.maxpat`, `fragment.maxpat`, `fragmentOLD.maxpat`, `knobspeed.maxpat`, `midilearn.maxpat`, `noise.maxpat`, `pend.spread.maxpat`, `pendulumOLD.maxpat`, `pluggo.dryWet2~.maxpat`, `pluggo.dryWet3~.maxpat`, `rchoose.maxpat`, `rchoosef.maxpat`, `spectralharmOLD.maxpat`, `speed.rsc2~.maxpat`, `transRatio.maxpat`. (The `.amxd` devices are loaded by `amxd~` at runtime and so are not in this static list.) + +## Appendix B — `MakeNoiseDay/` files reachable from nothing (including from Lamplight2026) + +`code/clock.gendsp`, `code/euclidseq.gendsp`, `patchers/Euclid.maxpat`, `ImprovBox.maxpat`, `ImprovBox2.maxpat`, `chordseq.maxpat`, `euclid_internal.maxpat`, `euclid_ui.maxpat`, `isalive.maxpat`, `lfo~.maxpat`, `miditocv.maxpat`, `scaleseq.maxpat`, `synclfo~.maxpat`, `temposlider.maxpat`, `trigger~.maxpat`, `volseq.maxpat`. Used only by `MakeNoiseDay.maxpat` itself: `autograin`, `scalemapper`. Used by Lamplight2026 (via `knobgrain.maxpat`): `scaleclip`, `wakemove`. + +## Appendix C — method notes + +- LOC = `wc -l` over `git ls-files`; "code LOC" excludes `.md`, `.json`, `.gitignore`. +- Commit counts/dates re-run after `git fetch --unshallow` (207 commits from 2026-01-24). +- Max reachability: parsed each `.maxpat` as JSON, collected `box.text` first tokens and bpatcher `box.name`, followed edges to same-named local files transitively. Binary-framed `.maxpat`/`.amxd` were compared with `cmp`, `diff` and byte-offset search in Python. +- Reference greps excluded `.git/` and `__pycache__/`. diff --git a/docs/audit/2026-11-09/detail/fundingcaptcha.md b/docs/audit/2026-11-09/detail/fundingcaptcha.md new file mode 100644 index 0000000..2deb1eb --- /dev/null +++ b/docs/audit/2026-11-09/detail/fundingcaptcha.md @@ -0,0 +1,487 @@ +# FundingCAPTCHA audit — `ShowControl/FundingCAPTCHA/` + +Read-only analysis, 2026-11-09. All paths are relative to `ShowControl/FundingCAPTCHA/` unless prefixed. `images/private/` does not exist in this checkout and was not listed. + +Goal lens: small files, isolated reasoning, no dead code, no stale docs. + +--- + +## 0. Headline + +- **10,992 LOC Python** (5,580 source / 5,412 tests), 3,257 LOC GLSL, 428 lines JSON. +- The directory is really **three products sharing one folder**: (a) the BodyCaptcha kiosk (`app.py` + `games/` + `arc.py` + `body_grid.py`), (b) the shader player (`shader_app.py`, `playlist_app.py`, `shadertoy.py`, `body_blobs.py`, `shaders/`), (c) the level-authoring toolchain (`bodycaptcha_editor.py`, `crop.py`, `distribution/`, `BodyCaptchaEditor.command`). Plus one orphaned dev tool (`body_grid_tester.py`). +- **Confirmed dead**: `games/body_keepaway.py` (498 LOC, never loaded: `app.py:349-351`), ~45 LOC in `games/grid.py`, `ScreenSavers/waiting.py` (stub superseded by `pipes_3d`), 16 of 24 LFS images under `images/yeavl_may/`, `distribution/` (297 LOC, no commits since May, superseded for Mac by the `.command`, Windows build never re-shipped). +- **Live bug**: `app.py:551` `_save_local({})` overwrites `captcha-settings.local.json` with `{}` on every `R` keypress / `/api/restart`, wiping per-host overrides. Leftover from the corner-cal era (ADR-0012), never removed when ADR-0013 deleted CORNER_CAL. +- **Biggest duplication**: camera thread + monitoring server + settings loader exist twice (`app.py` vs `body_grid_tester.py`, ~160 LOC verbatim-ish), and the shader behaviour tests reimplement a GL test rig **five times** (~350 LOC of copy-pasted fixtures/helpers). +- **Docs**: `docs/FundingCAPTCHA.md` describes a monitoring HTML page, an inbound `/captcha/mode` OSC address, and a non-gitignored local settings file — none exist. `docs/games/*.md` and ADR-0012/0013 describe pre-ADR-0019 UI and pre-#150 hold mechanics. CONTEXT.md still says `app.py` "loads all Games" including UpsideDown/Rhythm/Keepaway. +- **Estimated deletable now**: ~1,500 LOC (conservative, keeps tester and editor); ~2,100 if `body_grid_tester.py` is folded into `app.py --tester` and `distribution/` is removed. + +--- + +## 1. Inventory + +### Entry points + +| File | LOC | Classification | Evidence | +|---|---|---|---| +| `app.py` | 756 | **Production show path** | `deploy/captcha.service:11` `ExecStart=… app.py --camera --port 8080` | +| `shader_app.py` | 370 | **Production (alternate show mode)** + library for `playlist_app` | docs/FundingCAPTCHA.md:74 "point ExecStart at shader_app.py"; `playlist_app.py:35-36` imports 6 names from it | +| `playlist_app.py` | 421 | **Production (alternate show mode)**, newest (#169, 2026-11-08) | imports `app.load_settings`, `shader_app.*`; supersedes `shader_app.main()` as the richer rotation (Setups + videos) | +| `body_grid_tester.py` | 626 | **Dev-only tool / near-abandoned** | 3 commits (2026-05-12 → 2026-09-06; the last is a search-replace in #150). Own copies of settings loader, monitoring server, camera thread. Reads config keys (`grid`, `activation_rule`) that exist in no settings file. | +| `bodycaptcha_editor.py` | 847 | **Operator / content-author tool** | `BodyCaptchaEditor.command` (macOS launcher, #152 2026-09-30); `docs/FundingCAPTCHA.md:313` | +| `ScreenSavers/pipes_3d.py` | 412 | Production (screensaver) + has own `__main__` preview (`:427-454`) | listed in `screensavers.json` | +| `ScreenSavers/waiting.py` | 42 | Production but **stub** ("Stub screensaver … Replace with generative visuals", `:1`) | listed in `screensavers.json` | +| `distribution/build.py` | 67 | **Dev-only / probably abandoned** | PyInstaller Windows build; the three scripts were created in one commit (#94, 2026-05-23) and never changed; only `distribution/README.md` was touched since (#152, 2026-09-30, adding the Mac run-from-source path). `dist/` gitignored. README says re-run after every editor change — editor changed 3× in Sept (#143–#145) with no rebuild trail. | +| `distribution/package.py` | 101 | same | | +| `distribution/merge_levels.py` | 129 | **Operator tool / probably abandoned** | no tests; only referenced from `distribution/README.md` and `docs/FundingCAPTCHA.md:128` | + +### Library modules + +| File | LOC | Purpose | Imported by | +|---|---|---|---| +| `arc.py` | 129 | `Arc`, `ArcStatus`, `Game` interface, `_NoGame`, `ShuffleBag`, `blend_intensity` | `app.py:47`, `shader_app.py:38`, `playlist_app.py:34`, `games/bodycaptcha.py:24`, `games/body_keepaway.py:23`, `test_arc.py` | +| `body_grid.py` | 97 | `BodyGridActivator`: foreground depth frame → `{(col,row): [(slab_id, coverage)]}` | `games/bodycaptcha.py:22`, `games/body_keepaway.py:21`, `body_grid_tester.py:56`, `test_body_grid.py` | +| `silhouette.py` | 76 | `render_silhouette`, `build_cam_transform`, `apply_cam_transform` | `app.py:46`, `body_grid_tester.py:57`, `games/bodycaptcha.py:23`, `games/body_keepaway.py:22`, `test_silhouette.py` | +| `crop.py` | 62 | `crop_rect`, `crop_scale`, `ANCHORS` — aspect-preserving crop | `bodycaptcha_editor.py:12`, `games/bodycaptcha.py:25`, `test_crop.py` | +| `shadertoy.py` | 468 | Shadertoy-compatible multipass GL renderer: `load_setup`, `wrap_source`, `pack_body`, `BodyMouse`, `Keyboard`, `js_keycode`, `Renderer`, `_PingPong`, BLOB_* constants | `shader_app.py:40`, `playlist_app.py:37`, `body_blobs.py:31`, 7 test files | +| `body_blobs.py` | 207 | `BodyBlobs`/`BlobConfig`/`find_blobs`: connected components + `IIVision.BlobStabilizer` → 8×3 record texture | `shader_app.py:39`, `test_body_blobs.py`, `test_hunter_bird.py`, `test_watering_can.py`, `test_rainbow_poop.py` | +| `games/grid.py` | 258 | `Grid` letterbox geometry + ADR-0019 shell renderers (`draw_top_bar`, `draw_hold_ring`, `draw_center_text`) + font caches + colour constants | `games/bodycaptcha.py:16`, `games/body_keepaway.py:16` | +| `games/bodycaptcha.py` | 477 | BodyCaptcha Game: level bag, hold/drain, blow-up confetti, drawing, hot-reload, `--levels` override | `app.py:357` (importlib, by filename), 5 test files | +| `games/body_keepaway.py` | 498 | Keepaway Game (4 commits, 2026-05-22 #90 → 2026-06-04; unplugged from the Arc in #142, 2026-09-06) | **nothing** (see §2) | + +### Tests (5,412 LOC) + +| File | LOC | Tests | Needs GL | +|---|---|---|---| +| `test_watering_can.py` | 1097 | shader `watering_can` behaviour | yes | +| `test_hunter_bird.py` | 667 | shader `hunter_bird` behaviour | yes | +| `test_endless_sunset.py` | 601 | shader `endless_sunset` behaviour | yes | +| `test_shadertoy.py` | 465 | `shadertoy.py` + `shader_app.Rotation` + blob painter | partly | +| `test_rainbow_poop.py` | 439 | shader `rainbow_poop` behaviour | yes | +| `test_bodycaptcha_editor.py` | 218 | editor crop/align/levels path | no (SDL dummy) | +| `test_playlist_app.py` | 203 | playlist + video decode | yes + `av` | +| `test_body_grid.py` | 179 | activator | no | +| `test_bodycaptcha_levels.py` | 177 | levels I/O + override | no | +| `test_bodycaptcha_hold.py` | 151 | hold/drain | no | +| `test_body_blobs.py` | 148 | tracker records | no | +| `test_crop.py` | 132 | crop geometry | no | +| `test_silhouette.py` | 129 | render + reproject | no | +| `test_arc.py` | 117 | Arc/ShuffleBag/blend | no | +| `ScreenSavers/test_pipes_3d.py` | 98 | vines smoke | no | +| `test_bodycaptcha_bg.py` | 77 | bg crop | no | +| `test_app_games.py` | 51 | `_load_games` | no | + +### Data / assets + +| Path | Status | +|---|---| +| `captcha-settings.json` (74) | live, read by `app.py`, `shader_app.py`, `playlist_app.py`, `body_grid_tester.py`, games | +| `bodycaptcha-levels.json` (230, 9 levels) | live | +| `taunts.json` (27) | live (`games/bodycaptcha.py:289`) | +| `keepaway-body-levels.json`, `keepaway-body-taunts.json` | read only by dead `body_keepaway.py` | +| `screensavers.json` | live (`app.py:382`) | +| `playlist.json` | live (`playlist_app.py:41`) | +| `shaders/setups.json` | live (`shader_app.py:149`, `test_shadertoy.py:179`) | +| `shaders/{ripples,endless_sunset,hunter_bird,watering_can,rainbow_poop}/` | all in rotation | +| `shaders/blob_debug/` | reference reader, not in rotation (intentional, `setup.json:_comment`) | +| `shaders/body_painter.glsl`, `blob_painter.glsl` | live (`shadertoy.py:51-52`) | +| `images/jess.jpg` + 8 of `images/yeavl_may/*.jpg` | referenced by levels | +| **16 other `images/yeavl_may/*.jpg`** | **unreferenced** (24 files in dir, 8 referenced) — LFS blobs, ~1 commit each in May | +| `videos/` | gitignored, README only | +| `deploy/` | live | +| `BodyCaptchaEditor.command` | live (macOS) | + +--- + +## 2. Dead code + +Verified by grep over the whole repo (`the repo root`), not just this directory. + +### Whole files / directories + +1. **`games/body_keepaway.py` (498 LOC)** + `keepaway-body-levels.json` (22) + `keepaway-body-taunts.json` (17) + `captcha-settings.json:44-49` `keepaway_body` block. `app.py:349-351` hardcodes `for name in ("bodycaptcha",)`. `test_app_games.py:26-28` asserts Keepaway is *not* loaded. Nothing else imports the module. "Retained for a future return" (app.py:349) — git history is the retention mechanism; this is ~540 LOC the Arc never reaches, which every reader of `games/` still has to rule out. +2. **`ScreenSavers/waiting.py` (42 LOC)**. Self-described stub. `pipes_3d.py:382-402` already draws the identical title + "Step into frame to begin" overlay. Having both in `screensavers.json` means 50% of idle time is the stub. Delete and drop from `screensavers.json`. +3. **`distribution/` (297 LOC Python + ~150 lines of README/TXT)**. Evidence of abandonment: all three scripts date from one commit (#94, 2026-05-23) and have never been edited; the editor had 3 substantive changes in Sept (#143, #144, #145) with no rebuild/resend trail; `BodyCaptchaEditor.command` (#152) is the newer teammate path and `distribution/README.md:30-36` says Mac users "commit on a branch instead of using the zip/merge flow". `merge_levels.py` has no tests. The `sys.frozen` branch in `bodycaptcha_editor.py:16-19` exists only for this. **Confirm with the owner**; if no Windows teammate is active, delete the directory and the frozen-path branch. +4. **16 unreferenced images in `images/yeavl_may/`**. `bodycaptcha-levels.json` references `jess.jpg` + 8 yeavl_may files; the directory holds 24. All images landed 2026-05-10 → 2026-05-30 (3 commits) and none has been referenced since. These are LFS objects pulled on every Pi install (`deploy/install.sh:30`). + +### Dead members inside live files + +| Location | What | Why dead | +|---|---|---| +| `app.py:90-91`, `:551` | `_save_local()` and its one call | Writes `{}` to `captcha-settings.local.json`. Nothing reads it back except `load_settings` on next boot — so this **destroys per-host overrides**. Leftover from corner-cal persistence (ADR-0012 era). Bug, not just dead. | +| `app.py:69-73` | `MID_GREY`, `LT_GREY`, `GREEN`, `YELLOW`, `RED` | `app.py` uses only `BLACK`, `WHITE`, `DK_GREY`, `CYAN` | +| `app.py:284-285` | `# ── Silhouette rendering ──` empty section header | leftover from `bf83c47` extraction | +| `app.py:104,133-149,152-163,189-194` | `/ws` status WebSocket + `broadcast()` + `_app_status` | Only consumer is `Dashboard/index.html:78` which polls `GET /health`. Nothing connects to `/ws` (`Dashboard/fundingcaptcha.html:49` uses `/logs` only; `index.html` uses HTTP). `broadcast()` is called 9× per loop iteration for no listener. Keep `/health` JSON; delete the `/ws` channel or find a client. | +| `games/grid.py:18-24` | `SUCCESS_COLOR`…`HIGHLIGHT_COLOR` (7 aliases) | zero references outside grid.py | +| `games/grid.py:26,90-91` | `CELL_PAD`, `Grid.inner_rect` | only used by `blit_cell` (dead) | +| `games/grid.py:67,93-115` | `Grid._flashes`, `Grid.flash()`, `Grid.draw_flashes()` | zero callers | +| `games/grid.py:97-102` | `Grid.draw_background()` | zero callers (both games draw cells inline) | +| `games/grid.py:117-120` | `Grid.blit_cell()` | zero callers | +| `games/grid.py:16` | `BLUE` | imported by `body_keepaway.py:18` but unused there (it defines `RUNNER_COLOR` locally) | +| `games/body_keepaway.py:19`, `bodycaptcha.py:20` | import `hud_font`, `top_bar_height` | neither game uses them (they read `g.top_reserve`); `hud_font` is grid-internal | +| `body_blobs.py:127-129` | `BodyBlobs.active` property | zero callers (grep `\.active\b`) | +| `body_grid.py:40,89-92` + `body_grid_tester.py:24,99` | `activation_rule="max"` | only the tester can select it, from a key (`activation_rule`) that exists in no settings file; games never pass it | +| `body_grid_tester.py:86` | `settings.get("grid", [4,4])` | key `grid` is not in `captcha-settings.json`; tester-only | +| `captcha-settings.json:6` `detection.min_blob_pixels` | read into `DetectionConfig` (`app.py:267`) | `BlobTracker.detect_foreground()` (`IIVision/blob_tracker.py:257-272`) never consults `min_blob_pixels`; only `detect()`/`_extract_blobs` do, which FundingCAPTCHA does not call. **No-op key** in this Element (and a different key of the same name under `shader_app.blobs` *is* live). | +| `shader_app.py` `main()` (`:308-366`) | the shader-only entry | Functionally a subset of `playlist_app.main()` with `videos: []`. Not dead today (docs point at it; `--setup` pin flag only exists here), but one of the two loops should go — see §4. | +| `captcha.service:6-7` comments | "`--mock-camera` to emit fake blobs" | stale wording (no blobs since ADR-0013) | + +### CLI flags + +- `app.py --test-depth` is documented and live. `body_grid_tester.py --cols/--rows/--debug/--port 8081` are live but undocumented anywhere (`docs/FundingCAPTCHA.md:249-254` shows only a bare invocation that, without `--camera`/`--mock-camera`, goes straight to LIVE with no frames: `body_grid_tester.py:512-513`). +- `playlist_app.py` has no `--setup` pin (only `shader_app.py:311` does) — the one reason `shader_app.main()` still earns its keep. + +--- + +## 3. Duplication + +### 3a. Camera thread — `app.py` vs `body_grid_tester.py` + +`app.py:218-281` `_camera_thread`/`_camera_inner` and `body_grid_tester.py:190-253` are the same algorithm (Calibrator → `BlobTracker.detect_foreground` → `apply_cam_transform` → `cam_q.put({"type": "foreground", …})`). Differences: tester lacks the `collect` standback gate (`app.py:240-251`), has a different `max_depth_mm` default (4000 vs 2500 — `tester:206` vs `app:269`), and additionally ships raw/delta arrays and calibration debug images (`tester:232-238, 246-253`). `shader_app.start_camera()` (`:186-212`) reuses `app._camera_thread` correctly — the tester should too. + +### 3b. Monitoring server — `app.py` vs `body_grid_tester.py` + +`app.py:104-213` and `body_grid_tester.py:105-185`: `_ws_clients`, `_mon_loop`, `_app_status`, `broadcast()`, `_send_ws()`, `_ws_handler()`, `_process_request()`, `_run_monitoring()` — ~80 LOC near-verbatim. Only the endpoint names differ (`/api/restart` vs `/api/debug/toggle`). + +### 3c. Settings loader — 2 copies + +`app.py:80-87` and `body_grid_tester.py:75-82` are byte-identical `load_settings()`. (`shader_app.py:37` and `playlist_app.py:33` import `app.load_settings` — but that import drags in the whole 756-LOC `app.py` module including pygame/asyncio/websockets imports and `sys.path` mutation at `app.py:42-44`, just for a 7-line function and `DIR`.) + +### 3d. Camera factory — 3 copies + +`app.py:486-494`, `body_grid_tester.py:489-497`, `shader_app.py:197-202`: `MockCamera(width=cam_cfg.get("width",640), height=cam_cfg.get("height",400), fps=cam_cfg.get("fps",10))` vs `OrbbecCamera(serial=…)`. Also note the hard-coded default `height=400` in all three while `captcha-settings.json:70` says 480. + +### 3e. Depth-slab / ROI defaults — 7 copies + +`{"near_mm": 800, "far_mm": 2500, "slab_id": 0}` and `{"x":0,"y":0,"w":640,"h":400}` appear at `app.py:436-440`, `shader_app.py:240-243`, `body_grid_tester.py:90-91`, `games/bodycaptcha.py:177-178,200-201`, `games/body_keepaway.py:150-152,205-206`. Slab colour fallback `[0, 220, 100]` appears at `app.py:442`, `bodycaptcha.py:410`, `body_keepaway.py:410`, `body_grid_tester.py:297,315,463`. These defaults also disagree with the committed settings (1000–4000 mm, 480×480 ROI), so the fallbacks are silently wrong if a key is dropped. + +### 3f. Slab-mask computation — 4 copies + +`(fg >= near) & (fg < far)` OR-folded over slabs: `silhouette.py:30-31`, `shadertoy.pack_body:169-170`, `body_grid.py:84` (per cell, with extra `cell > 0`), `body_grid_tester._draw_silhouette:313-317`. One `slab_mask(fg, slabs) -> bool array` would serve all four. + +### 3g. `_make_activator` — 2 copies + +`games/bodycaptcha.py:175-184` and `games/body_keepaway.py:148-157`: same function, keepaway adds a `defaults["grid"]` fallback. + +### 3h. Blow-up: `_Particle`, `_spawn_confetti`, `_draw_confetti`, `_begin_blowup` — 2 copies + +`games/bodycaptcha.py:64-78, 356-381, 467-473` vs `games/body_keepaway.py:48-61, 360-385, 488-494`. ~60 LOC each. Moot if keepaway is deleted; otherwise `games/blowup.py`. + +### 3i. Levels-path override — 2 copies + +`games/bodycaptcha.py:30-46` and `bodycaptcha_editor.py:20-32`: `_LEVELS_DEFAULT`, `_levels_path`, `set_levels_path`, `get_levels_path`. And `_img_load` (pygame → Pillow fallback): `games/bodycaptcha.py:142-156` vs `bodycaptcha_editor.py:39-47` (game version adds an LFS-pointer check the editor lacks). And `DEFAULT_LEVEL`: `bodycaptcha.py:109-110`, `bodycaptcha_editor.py:80-87`, `distribution/package.py:100-107` — three different defaults. + +### 3j. Rotation logic — `shader_app.Rotation` vs `playlist_app.Playlist` + +`shader_app.py:86-144` and `playlist_app.py:225-334`: `next()` single-item short-circuit, `for _ in range(max(1,len))` draw loop, `reload()`, `failed()` drop-from-bag, `_load_setup()` try/except → identical structure. `Playlist` is a strict superset. Tests duplicate too: `test_shadertoy.py:307-345` vs `test_playlist_app.py:128-203` (`test_render_failure_drops_setup`, `test_single_setup_is_not_*_on_expiry` exist in both). + +### 3k. Main loops — `shader_app.main` vs `playlist_app.main` + +`shader_app.py:330-362` and `playlist_app.py:365-411`: event handling (`N`/`→`/`R`/Esc + `stage.handle_key`), fps logging every 10 s, teardown — same. + +### 3l. Test rig — 5 copies + +| Piece | Copies | +|---|---| +| `ctx()` module fixture (EGL → fallback → CI fail/skip), 15 LOC | `test_shadertoy.py:157-171`, `test_endless_sunset.py:22-36`, `test_hunter_bird.py:27-41`, `test_watering_can.py:59-73`, `test_rainbow_poop.py:44-58` (`test_playlist_app.py:11` imports it from `test_shadertoy`) | +| `Rig` class (Renderer + BodyBlobs on fake clock; `run/buf/image/release`) | `test_hunter_bird.py:44-84`, `test_watering_can.py:76-112`, `test_rainbow_poop.py:61-114`; functional equivalent `make/run/buf/image` in `test_endless_sunset.py:39-72` | +| `rect()`, `EMPTY`, `flickering()`, `walking()`, `standing()`, `both()` | all four shader tests | +| GLSL-constant extraction | `test_watering_can.glsl_consts` (`:23-50`, a mini GLSL `const` evaluator with `eval`), `test_rainbow_poop.const` (`:27-34`, regex), `test_endless_sunset.make_with` (`:310-325`, regex rewrite of `common.glsl`); `test_hunter_bird.py:304` hardcodes `PRINT_LIFE_S = 4.0` instead | + +~350 LOC of boilerplate; one `shader_test_rig.py` (or `conftest.py`) would hold it. + +### 3m. Colour constant tables — 4 copies + +`BLACK/WHITE/DK_GREY/MID_GREY/LT_GREY/GREEN/YELLOW/CYAN/RED` in `app.py:65-73`, `body_grid_tester.py:61-68`, `games/grid.py:6-16`, `bodycaptcha_editor.py:50-67` (editor's values differ slightly), `arc.py:21` (`_DK_GREY` "matches app.DK_GREY"). + +--- + +## 4. Modularization opportunities + +### `app.py` (756 LOC) — mixes 6 concerns + +Current contents: settings (80-91), monitoring server (104-213), camera thread (218-281), test-input painter (288-339), plugin loading for games + screensavers (347-403), and a 350-line `main()` (408-752) that holds the whole BG_CAL/SCREENSAVER/GAME state machine as local variables and closures. + +Proposed split: + +| New module | Moves | Public surface | +|---|---|---| +| `settings.py` (~30) | `DIR`, `SETTINGS*`, `load_settings()`, plus the shared defaults from §3e (`DEFAULT_SLAB`, `default_roi(cam_cfg)`, `slab_color(settings)`) and `sys.path` setup | `load_settings() -> dict`, `DIR`, defaults | +| `monitor.py` (~90) | `_LogHandler`, `/health` JSON, `/logs` WS, `/api/restart` → `threading.Event`. Parameterise the command endpoints so the tester's `/api/debug/toggle` is the same class. | `MonitoringServer(port, commands: dict[str, Event]).start()`, `.set_status(dict)`, `.log_handler` | +| `camera_feed.py` (~90) | `_camera_thread`/`_camera_inner`, `make_camera(cam_cfg, mock)`, the `collect` standback gate, the `cam` dict + `_start_camera`/`_do_restart` closures (`app.py:524-553`) | `CameraFeed(settings, mode).start()/restart()/stop()`, `.queue`, `.collect` | +| `test_input.py` (~55) | `TestInputHandler` | unchanged | +| `plugins.py` (~60) | `_load_games`, `_load_screensavers`, `_NoSaver` (`_NoGame` already lives in `arc.py` — put `_NoSaver` beside it or here) | `load_games(settings, levels_path)`, `load_screensavers(settings)` | +| `kiosk.py` (~250) | `AppState`, the three state branches extracted to `_tick_bg_cal / _tick_screensaver / _tick_game`, with the 15 loose locals (`cal_elapsed`, `cal_started`, `cal_done_seen`, `attract_elapsed`, `saver_elapsed`, `sil_cache`…) as fields of a `Kiosk` dataclass. Also fixes the `R`-key vs HTTP-restart reset blocks being two slightly different copies (`app.py:583-591` resets `sil_cache`; `:598-604` does not). | `Kiosk(settings, args).run()` | +| `app.py` (~40) | argparse + wiring | `main()` | + +Then `shader_app.py`/`playlist_app.py` import `settings`/`camera_feed` instead of `app` (today `from app import DIR, _camera_thread, load_settings` pulls pygame display code and a private function across module boundaries). + +### `body_grid_tester.py` (626 LOC) + +Half of it is §3a–3c duplication. After the split above it becomes: `_build_config` (15), 5 draw functions (180), `main` (180). Two choices: +- **Fold into `app.py --tester`**: it is "app.py without games, with debug overlays". The extra camera payload (raw/delta/cal images) could become an opt-in `debug=True` on `CameraFeed`. +- **Keep standalone at ~300 LOC** using `settings`/`monitor`/`camera_feed`. Either way delete `activation_rule`/`grid` tester-only keys or add them to the settings file. + +### `bodycaptcha_editor.py` (847 LOC) + +One `Editor` class, 700 LOC, mixing data model, widget toolkit, and three screens. + +| New module | Moves | +|---|---| +| `editor/levels_io.py` (~60) | `DEFAULT_LEVEL`, `_load`, `_save`, `_sort_key`, levels-path override, `_all_image_paths`, `_img_load`. **Share with `games/bodycaptcha.py`** (§3i) — one `levels.py` at package root: `read_levels`, `write_levels`, `DEFAULT_LEVEL`, `set/get_levels_path`, `load_image`. | +| `editor/widgets.py` (~130) | `_label`, `_text_field`, `_stepper`, `_labeled_stepper`, `_btn_row`, `_btn`, `_wrap`, colour table | +| `editor/level_model.py` (~90) | the mutation methods `_toggle … _del_level`, `_align/_set_align`, `_grid_dims`, `_vset` — pure, testable without pygame | +| `editor/views.py` (~300) | `_draw_grid`, `_draw_picker`, `_sidebar`, `_align_pad`, geometry (`_cell_size`, `_preview_rect`, `_thumb_rects`) | +| `bodycaptcha_editor.py` (~80) | `Editor.run()` event loop + argparse | + +Also: `Editor.__init__` calls `pygame.display.set_mode` — tests (`test_bodycaptcha_editor.py:22-33`) have to open a dummy display to test crop maths; the model split fixes that. + +### `shadertoy.py` (468 LOC) + +Already has clear sections. Split along the line its own docstring draws ("Pure helpers … need no GL; Renderer needs a moderngl context"): +- `shadertoy/setup.py` (~90): `Setup`, `load_setup`, `wrap_source`, `_HEADER/_FOOTER`, `SOURCES`, `ShaderError` +- `shadertoy/body.py` (~70): `pack_body`, `BodyMouse` +- `shadertoy/keyboard.py` (~40): `js_keycode`, `Keyboard` +- `shadertoy/blobs_contract.py` (~10): `BLOB_SLOTS/ROWS/states` (so `body_blobs.py` stops importing the renderer module for 6 constants) +- `shadertoy/renderer.py` (~220): `_PingPong`, `Renderer`, `_idate` +Low urgency; the file is coherent. Do it only if `body_blobs → shadertoy` import direction bothers you. + +### `shader_app.py` + `playlist_app.py` (791 LOC together) + +`Playlist ⊇ Rotation`, `playlist_app.main ⊇ shader_app.main`. Proposal: +- Delete `Rotation` and `shader_app.main()`; give `playlist_app` a `--setup NAME` flag that builds a one-item playlist. `shaders/setups.json` and `playlist.json:setups` are also two lists of the same thing — keep `playlist.json` (it already carries `setup_duration_s`), delete `setups.json`, point `test_shadertoy.py:179` at the playlist. +- Rename what remains of `shader_app.py` → `shader_stage.py` (`ShaderStage`, `open_display`, `start_camera`, `drain_camera`, `add_mode_args`, `_TestMouse`) — a library, not an app. +- `playlist_app.py` then splits: `video_player.py` (`VideoPlayer`, `VideoScreen`, `fit_rect`, GLSL strings — 150 LOC) and `playlist_app.py` (`Item`, `Playlist`, `main` — 200 LOC). +- `docs/FundingCAPTCHA.md:74` and `deploy/captcha.service` comment then point at one shader entry point. + +### `games/body_keepaway.py` (498) / `games/bodycaptcha.py` (477) + +If keepaway is deleted, bodycaptcha is the only game and the remaining split is: `games/levels.py` (shared with editor, §above), `games/blowup.py` (`_Particle`, confetti spawn/draw — 60 LOC, reusable by any future Game), leaving `bodycaptcha.py` ~350. If keepaway stays, also extract `games/activator.py` (`_make_activator`) and let both import `blowup`. + +### `games/grid.py` (258) + +After deleting the dead members (§2) it is ~200 LOC and splits naturally: `grid.py` (`Grid`, ~60) and `shell.py` (ADR-0019 renderers + caches + `hud_font`, ~130). ADR-0019 itself suggested `games/shell.py`. + +### The shader behaviour tests (`test_watering_can.py` 1097, `test_hunter_bird.py` 667, `test_endless_sunset.py` 601, `test_rainbow_poop.py` 439 = 2,804 LOC) + +**Are they reimplementing shaders in Python?** Partly, and that part is the fragile part. + +What they *do* (good): run the real GLSL on a real (llvmpipe) GL context with the real `BodyBlobs` tracker on synthetic masks, then read buffers back and assert behaviour ("walking rewinds growth", "standing never strikes", "splat lands on the head"). That is black-box behaviour testing of team-authored content and it has caught real bugs (the `--test-input` keyboard texture fix noted in ADR-0022). + +What is unsustainable: +1. **Buffer-layout knowledge is duplicated into the tests.** `test_watering_can.Rig.person/base/ghost/sun` (`:102-105`), `test_hunter_bird.Rig.slot/head/attn/reticle/mood/blink/stamp/prints` (`:70-77`), `test_rainbow_poop.Rig.slot/anchor/poop/drop/splats/grounds` (`:99-104`) each mirror the texel map at the top of the Setup's `common.glsl`. Any layout change breaks dozens of tests. These are implementation-detail tests of storage, not of what the visitor sees. +2. **Shader maths re-derived in Python.** `test_watering_can.py:318-329` `grown()/seconds_to()` reimplement the GLSL growth curve from `GROW_EARLY`, `GROW_EARLY_S`, `GROW_S`; `test_rainbow_poop.py:40-41` `FALL_S = (SPAWN_Y - HEAD)/FALL_SPEED`; `test_hunter_bird.py:100` `walking()` docstring computes "motion ≈ 0.57 so lock stays < 1 for 6 s at the defaults". When a tunable is retuned, the tests follow only if the Python formula still matches the GLSL formula — that's the reimplementation trap. `test_watering_can.glsl_consts` goes furthest: a 25-line GLSL `const` expression evaluator using `eval`. +3. **Three different mechanisms** for reading GLSL constants (§3l). Pick one and ship it as a helper. +4. **Magic tolerances everywhere** (`< 0.06`, `< 0.15 * …`, `>= merged - 0.05`, `abs(fall - 1/LEAVE_S) < 0.15/LEAVE_S`) tuned to llvmpipe half-float behaviour. `test_watering_can.py:271-292` even parametrises on slot 7 "the coarsest half-float spacing". These will flake on a different Mesa version. +5. **Scale**: 1,097 LOC to test a 1,069-LOC shader is a 1:1 ratio; each test runs tens to hundreds of GL frames; `test_hunter_bird.BigRig` renders a 480×480 Body ×40 frames ×3. CI time will balloon with each new Setup. + +**Recommendation:** +- Keep the GL behaviour tests, but shrink each to the **spec-level claims** in its design doc (`docs/superpowers/specs/*.md`, ~100 lines each): ~10-15 tests per Setup, not 40-75. Delete the layout-probing tests (`test_owner_is_record_slot`, `test_run_extents_span_leg_to_shoulder`, `test_slot_tracks_record_id`, `test_records_layout…`) or move them to one `test_shader_contracts.py` that checks only the *inputs* contract (Buffer A, blobs texture) which is what `shadertoy.py`/`body_blobs.py` own. +- Extract `shader_test_rig.py`: `ctx` fixture, `Rig`, mask builders (`rect/person/walking/flickering/both`), `glsl_consts()`, `read_buffer()`. Target ~200 LOC shared, removing ~350 LOC of copies. +- Move the five test files into `shaders//test_.py` next to the GLSL they test (pytest will still collect them via `testpaths = ShowControl`), so a Setup is a self-contained folder and a deleted Setup takes its tests with it. +- Stop re-deriving curves in Python: where a test needs "growth after 10 s", read it from the shader (render 10 s, record the value once as an approved fixture) rather than from a Python copy of the formula — or assert monotonicity/ordering, not values. +- Add a frame-count budget: a single `pytest -m gl --durations` check in CI so the suite can't double silently. + +--- + +## 5. Config sprawl + +### Files + +`captcha-settings.json` (+ optional `captcha-settings.local.json`, shallow-merged — **gitignored**, contrary to docs), `bodycaptcha-levels.json`, `taunts.json`, `keepaway-body-levels.json`, `keepaway-body-taunts.json`, `screensavers.json`, `playlist.json`, `shaders/setups.json`, `shaders/*/setup.json`, `deploy/private-assets.example.env`, `ShowControl/network.json` (via OSCFabric). + +### `captcha-settings.json` key → readers + +| Key | Readers | Notes | +|---|---|---| +| `detection.depth_delta_mm` | `app.py:266`, `tester:203` | live (`BlobTracker.threshold_map`) | +| `detection.min_blob_pixels` | `app.py:267`, `tester:205` | **no-op**: `detect_foreground` ignores it (`IIVision/blob_tracker.py:257-272`) | +| `detection.min_depth_mm` | `app.py:268`, `tester:205` | live (`Calibrator` valid mask, `blob_tracker.py:190`) | +| `detection.max_depth_mm` | `app.py:269`, `tester:206` | **likely no-op** for foreground path (`blob_tracker.py:186` "No upper cap"; `:300` is in `detect()`); defaults disagree 2500 vs 4000 | +| `depth_slabs[]` | `app.py:436`, `shader_app.py:240`, `tester:91`, `bodycaptcha.py:178,200`, `body_keepaway.py:151,205` | live | +| `slab_styles` | `app.py:441`, `tester:463`, `bodycaptcha.py:409`, `body_keepaway.py:409` | live, but every reader hardcodes `["0"]` except the tester; multi-slab styling is tester-only | +| `cell_activation_threshold` | `tester:98`, `bodycaptcha.py:180`, `body_keepaway.py:153` | live | +| `camera_roi` | `app.py:438`, `shader_app.py:241`, `tester:90`, games | live | +| `silhouette_dilation_px` | `app.py:236`, `tester:211` | live | +| `attract_dwell_s`, `min_foreground_pixels`, `silhouette_opacity` | `app.py:443-445`; `min_foreground_pixels` also `shader_app.py:256` | live | +| `calibration_frames` | `app.py:232,564`, `tester:209,525` | live | +| `calibration_countdown_s` | `app.py:450` | live, app only | +| `calibration_standback_s` | `app.py:451`, `shader_app.py:206` | live | +| `bodycaptcha.{timer_s,hold_s,hold_decay_rate,hint_opacity,intensity_weights}` | `bodycaptcha.py:197-199,242-266` | live | +| `keepaway_body.*` | `body_keepaway.py` only | **dead** | +| `shader_app.render_scale`, `.fps` | `shader_app.py:237,318`, `playlist_app.py:350` | live | +| `shader_app.blobs.*` (7 keys) | `shader_app.py:255` → `BlobConfig.from_dict` | live; `from_dict` silently drops unknown keys (`body_blobs.py:49-51`) so a typo is invisible | +| `camera.serial/width/height/fps` | `app.py:488-494`, `shader_app.py:197-202`, `tester:491-497` | live; code default height 400 ≠ file 480 | +| `camera.pos_cm`, `camera.rotation` | `silhouette.py:47-48` | live | +| `_comment`, `_silhouette_dilation_px` | — | documentation keys (fine, but the convention is inconsistent: one has `_` prefix per key, others a block `_comment`) | + +### Keys read but absent from any file + +- `grid` (`tester:86`), `activation_rule` (`tester:99`) — tester-only. +- `keepaway_body.grid` default `[8,6]` — dead. + +### Two JSON lists of Setups + +`shaders/setups.json` (shader_app) and `playlist.json:setups` (playlist_app) must be kept in sync by hand; today they match. See §4. + +### Defaults scattered in code + +Every `settings.get(key, default)` site carries its own default; §3e counted 7 copies of slab/ROI defaults with values that disagree with the committed file. A single `settings.py` with a `DEFAULTS` dict (or a dataclass) is the fix; then `captcha-settings.json` can be validated against it at startup and typos (`shader_app.blobs.*`) surface as warnings. + +--- + +## 6. Docs drift + +### `docs/FundingCAPTCHA.md` + +| Line | Claim | Reality | +|---|---|---| +| 11, 162-170 | "lightweight HTTP/WebSocket monitoring server"; "`http://:8080/` — small HTML page with state + live log stream… Current Game + Level, Live intensity value, Last 100 log lines" | `GET /` returns a JSON blob (`app.py:189-194`). No HTML, no game/level/intensity in `_app_status` (the only fields ever broadcast are `state`, `progress`, `phase`, `remaining_s`, `fg_pixels`, `error`), no log history buffer (`_LogHandler` streams live only). The HTML lives in `ShowControl/Dashboard/fundingcaptcha.html`. | +| 93 | `captcha-settings.local.json` "not gitignored, but per-host" | It **is** gitignored (`.gitignore:19`, under a stale comment about `touch_test_pygame.py`). And `app.py:551` wipes it on restart. | +| 108 | "Per-game blocks: `bodycaptcha`, `keepaway_body` (timer_s, hold_s, hold_decay_rate, grace_s, abandon_s, weights…)" | keepaway never loads; `hint_opacity` omitted from the list | +| 137 | Keepaway "Module and its Level/taunt data retained for a future return" | accurate, but the retention is the problem (§2) | +| 146 | `pipes_3d.py` "(Windows-style 3D pipes screensaver)" | it is a garden-vines reskin (`pipes_3d.py:1-5`) | +| 152 | "sends to the TreeHouse Hub **each frame**" | intensity is throttled to 10 Hz (`app.py:741`) | +| 158 | `/captcha/mode` `s` (in) "from Dashboard" | `app.py` has no OSC receiver; `FabricClient` is outbound only. Nothing in the directory listens on `network.json` `captcha.osc_port 9003`. | +| 249-254 | `python3 body_grid_tester.py` | without `--camera`/`--mock-camera` it enters LIVE with no frames (`tester:512-513`); flags `--cols/--rows/--debug/--port 8081` undocumented; keys `D`/`C` undocumented | +| 128 | `distribution/` Windows editor build | probably abandoned (§2) | +| 74 | shader player "Not yet verified on the Pi 5 under kmsdrm" | written 2026-10-30; four Setups and a playlist player later, nobody has updated this. Either verify or keep — but a doc that says "unknown" for weeks is noise. | +| 40-48 | `app.py` flags table | omits `--levels` (documented only at :306) | + +### `docs/games/bodygrid.md` + +- "`BodyGridActivator` class (in `games/grid.py`)" — it is in `body_grid.py`. +- "Returns a set of `(col, row)` pairs" — returns `dict[(col,row) → [(slab_id, coverage)]]` (`body_grid.py:43`). +- "Standalone tester … logs active cells each frame" — it broadcasts a count over a WebSocket nobody connects to (`tester:590`). +- "Grid dimensions and `cell_activation_threshold` are configurable per-Level" — threshold is global only (`bodycaptcha.py:180`). + +### `docs/games/bodycaptcha.md` + +- "Status: In implementation (2026-05-13)" — shipped. +- "Hold timer **resets** if any extra cell is covered" — it drains at `hold_decay_rate` since #150 (`bodycaptcha.py:349-350`). +- Visual table: "Hold active → Green + hold progress bar shown" — ADR-0019 replaced the bar with a perimeter ring (`grid.py:193-203`). +- Config block omits `hold_decay_rate`; default `hint_opacity` shown as 0.1, file says 0.2 (`captcha-settings.json:40`). +- Level format omits `crop_align`. + +### `docs/games/keepaway.md` + +- "Status: Implemented as … body_keepaway.py" — implemented but **not loaded** (`app.py:349`). +- API block shows `update(self, activations, foreground, dt)` — actual is `update(foreground, dt)` (`body_keepaway.py:267`). +- "Timer: Countdown bar across the top … green → yellow → red" — now the ADR-0019 numeric timer (`body_keepaway.py:463`). + +### `docs/games/rhythm.md`, `upsidedown.md` + +Design notes for code deleted in ADR-0013 (2026-05), "Port TODO" ever since. Either track them as issues or delete; as docs they describe nothing in the tree. + +### `CONTEXT.md:76` + +"Examples: BodyCaptcha, UpsideDown, Rhythm, Keepaway. `app.py` loads all Games and plays them in shuffle-bag order." — one Game exists and is loaded; three of the four names are deleted or dead. + +### `AGENTS.md` / `docs/agents/running-and-testing.md:6` + +"`make test` runs Node tests (FundingCAPTCHA)" — the only Node test ever, `touch-input.test.js`, was deleted with the browser stack on 2026-05-10 (`4caa60b`). The only `.js` left in the repo is `FlowerBeds/layout_tool/layout.js` and there is no `package.json`, so `node --test` tests nothing. + +### Git-history context (full history, 207 commits from 2026-01-24) + +`ShowControl/FundingCAPTCHA` was rebuilt in the May 2026 pre-show crunch: browser stack deleted 2026-05-10, touch calibrator added and removed the same month, `screen_projector.py`/`pairs.json`/`rhythm-images.json`/`keepaway-images.json` deleted 2026-05-24, Keepaway ported 2026-05-22 (#90) then unplugged 2026-09-06 (#142). Since June the only churn in the kiosk path has been #142–#152 (Sept); everything since 2026-10-30 is the shader player. `games/grid.py`, `body_grid.py`, `ScreenSavers/*`, `keepaway-body-*.json` and all images have not changed since May. + +### ADRs + +**ADR-0012** (unified pygame app): describes `AppStateMachine BG_CAL → CORNER_CAL → LIVE`, Games "UpsideDown, Rhythm, Keepaway", `pairs.json`/`rhythm-images.json`/`keepaway-images.json`, "WS /ws → pushes blobs, touch state", "Dashboard … touch debug visualisation". All superseded by ADR-0013 but ADR-0012 carries no "Superseded/Amended by" header (ADR-0013 does say it supersedes 0004/0011, not 0012). `POST /api/restart` is actually method-agnostic (`app.py:181` matches path only). + +**ADR-0013** (silhouette interaction): +- "Hold timer … Covering any extra cell resets the hold timer" — drain since #150. +- "Blob detection is not used by FundingCAPTCHA" — true for `app.py`; false since ADR-0022 (`body_blobs.py:30` runs `BlobStabilizer`). ADR-0022 amends 0021, not 0013. +- "`camera_roi` (crop, display-space coordinates)" vs `docs/FundingCAPTCHA.md:102` "in camera pixel coords" — contradictory; code crops the post-transform frame (`body_grid.py:62`), i.e. display-space-at-camera-resolution. +- "BodyGrid is the first Game built" — the Game is BodyCaptcha; Body Grid is the input layer (CONTEXT.md:85 warns about exactly this conflation). +- Level format omits `hold_decay_rate`, `crop_align`. +- Consequences list "UpsideDown, Rhythm, and Keepaway must be ported… Code deleted pending port" — six months on, only Keepaway was ported and then unplugged. + +**ADR-0016** (test input): accurate except "Brush radius … ~20px" (code: 15, `app.py:296`). Good ADR. + +**ADR-0019** (UI shell): "`draw_hud_label`, `draw_progress_bar` become unused and should be deleted" — done. But it left `Grid.flash/draw_flashes/blit_cell/inner_rect/draw_background/CELL_PAD` and the 7 colour aliases behind (§2). Its "Implementation choice (helpers in grid.py vs games/shell.py) is left to the implementer" — implementer chose grid.py; §4 suggests finishing the move. + +**ADR-0021** (shader player): accurate. "Not yet measured on the Pi 5: frame rate per Setup" — still unmeasured per docs. Doesn't mention `playlist_app.py` / `setup_duration_s` override (only `docs/FundingCAPTCHA.md:78-87` does) — fine as the ADR predates it, but a one-line amendment note would help. + +**ADR-0022** (blob records): accurate against `body_blobs.py` and `shadertoy.py`. "Tracking runs only while the current Setup binds blobs, and resets when a Setup that doesn't is playing" — code resets on every load via `on_load=stage.blobs.reset` (`shader_app.py:325`), which is stronger than stated; fine. + +### Deploy + +- `deploy/captcha.service:1` `Description=FundingCAPTCHA Game Server` and `:7` "Use `--mock-camera` to emit fake blobs" — `server.py` era wording. +- `.gitignore:18-19` comment "written by touch_test_pygame.py" — that file no longer exists. + +--- + +## 7. Test coverage gaps and test quality + +### No tests at all + +| Module | LOC | Risk | +|---|---|---| +| `app.py` main loop / state machine | ~350 | The show path. BG_CAL→SCREENSAVER→GAME transitions, standback/countdown gating (`:649-687`), attract dwell, OSC throttling, R/HTTP restart — zero coverage. Only `_load_games` is tested (`test_app_games.py`). The `_save_local({})` bug would have been caught by any restart test. | +| `app.py` monitoring server | ~110 | untested; `/ws` has no client either | +| `app.py` `TestInputHandler` | 55 | untested | +| `app.py` `_camera_thread` | 65 | untested (needs a fake camera; `MockCamera` exists) | +| `body_grid_tester.py` | 626 | untested | +| `games/body_keepaway.py` | 498 | untested (and dead) | +| `games/grid.py` | 258 | untested — `Grid` letterbox maths, `_fit_prompt_font`, `_wrap_to_width`, `draw_center_text` binary search; pure-ish and cheap to test | +| `games/bodycaptcha.py` `_LevelBag` | 25 | untested — the difficulty-progression rule ("same or +1", fallback to all) is a spec claim in ADR-0013 with no test | +| `games/bodycaptcha.py` blow-up / WIN_FLASH / DONE transitions | ~80 | `test_bodycaptcha_hold.py` covers PLAYING→WIN_FLASH only; timer expiry → BLOWUP → DONE → `"loss"` is untested | +| `shader_app.ShaderStage`, `_TestMouse`, `start_camera`, `drain_camera`, `open_display` | ~200 | untested except `Rotation` | +| `playlist_app.VideoScreen`, `main` | ~100 | untested | +| `ScreenSavers/waiting.py` | 42 | untested (and a stub) | +| `distribution/*.py` | 297 | untested | +| `silhouette.build_cam_transform` | 17 | untested | +| `shadertoy.Renderer.release` / `_PingPong` lifecycle | | untested (GL resource leaks on Setup reload would only show on the Pi) | + +### Tests that test implementation details / are brittle + +- **All four shader behaviour tests** read private renderer state: `rig.r._buffers[name].read_tex` (`test_watering_can.py:99`, `test_hunter_bird.py:67`, `test_endless_sunset.py:66`, `test_rainbow_poop.py:96`) — a private attribute of `Renderer`. Add `Renderer.read_buffer(name)` as public API (it already has `read_body()`). +- Texel-layout probing (§4): `test_watering_can.py:236-240 test_owner_is_record_slot`, `:218-224 test_run_extents_span_leg_to_shoulder`, `:243-253 test_walker_leaves_no_hold_trail` (asserts `floor(0.9/HOLD_Q)*HOLD_Q/0.9` encoding), `test_hunter_bird.py:125-129 test_slot_tracks_record_id`, `test_rainbow_poop.py:307-314 test_ages_keep_time_in_half_floats`. These pin storage decisions. +- Half-float tolerance tuning: `test_watering_can.py:271-273` parametrises slot 7 "coarsest half-float spacing"; `:598` "a half float stalls the easing short of full"; `:610` "a half float stalls small steps". These encode llvmpipe f16 behaviour; a Mesa upgrade can flip them. +- `test_watering_can.glsl_consts` (`:23-40`) uses `eval()` on GLSL source — a mini-interpreter inside a test file. +- `test_endless_sunset.make_with` (`:310-325`) rewrites `common.glsl` with regex and recompiles — tests depend on the exact `const float NAME =` formatting of the shader. +- `test_hunter_bird.py:304` `PRINT_LIFE_S = 4.0` hardcoded while the other three Setups read constants from GLSL. +- `test_bodycaptcha_editor.py:22-33 editor fixture` constructs the full `Editor` (opens a display) to test `_align()` string logic — the model/view split (§4) fixes this. +- `test_app_games.py:19-20` opens a pygame display at module import; `test_bodycaptcha_hold.py` and `test_bodycaptcha_levels.py` do the same because `BodyCaptchaGame.__init__` reads `pygame.display.get_surface()` (`bodycaptcha.py:202`). Pass `(WW, WH)` into the constructor instead. +- `test_bodycaptcha_levels.py:68,139` imports mid-file (`# noqa: E402`) — three test modules glued into one. +- `test_playlist_app.py:11` `from test_shadertoy import SOLID, ctx` — cross-test-module import; belongs in a `conftest.py`. +- Global-state leakage: `set_levels_path` is module-global in `games/bodycaptcha.py:30-36`; two test files need `autouse` fixtures to restore it (`test_bodycaptcha_levels.py:71-75`, `test_bodycaptcha_editor.py:138-141`), and `test_app_games.py:45-50` exists only to assert "override leaked from a previous test" didn't happen. A `Levels` object passed to the game would remove all three. +- `ScreenSavers/test_pipes_3d.py:16-21` reimplements `importlib` module loading that `app._load_screensavers` also does — a third copy of the loader. + +### CI + +`.github/workflows/tests.yml:25-26` installs Mesa so GL tests run; the `ctx` fixtures `pytest.fail` under `CI=1` if no context — good. `requirements-dev.txt` lacks `av` (so `test_playlist_app` video tests `importorskip` → silently skipped in CI; `:49,94`), `Pillow`, and `websockets` (fine today since both are lazily imported). The GL suite is already 2,800 LOC / several hundred rendered frames per test file; no duration budget. + +--- + +## 8. Deletable LOC estimate + +| Item | LOC | Confidence | +|---|---|---| +| `games/body_keepaway.py` + its JSON + settings block | ~545 | high (never loaded, asserted by test) | +| `body_grid_tester.py` duplicated infra (monitoring/camera/settings/colours) | ~170 | high (if shared modules exist) — or 626 if folded into `app.py --tester` | +| `distribution/` (3 scripts + READMEs + `sys.frozen` branch) | ~300 py (+150 docs) | medium — confirm no active Windows teammate | +| `ScreenSavers/waiting.py` | 42 | high | +| `games/grid.py` dead members + aliases | ~45 | high | +| `app.py`: `_save_local`, unused colours, empty section, `/ws` channel + `broadcast` | ~60 | high (`/ws`: medium — confirm no client) | +| `shader_app.Rotation` + `shader_app.main` + `setups.json` (after `playlist_app --setup`) | ~120 | medium | +| Duplicate camera factories / slab defaults / colour tables after `settings.py` | ~60 | high | +| Test rig boilerplate (5× `ctx`, 4× `Rig`, 4× mask helpers, 3× const readers) | ~350 | high | +| Layout-probing / half-float shader tests | ~200-400 | medium (judgement call per test) | +| `body_blobs.active`, keepaway-only imports, misc | ~15 | high | +| **Total** | **~1,500 conservative / ~2,100 aggressive** | of 10,992 Python LOC (14–19%) | + +Plus 16 LFS images and ~300 lines of stale docs (`docs/games/rhythm.md`, `upsidedown.md`, half of `keepaway.md`, ADR-0012's body). + +--- + +## 9. Ranked refactors (value ÷ effort) + +1. **Delete dead code + fix `_save_local` bug** (keepaway, waiting.py, grid.py members, app.py leftovers, unreferenced images, `detection.min_blob_pixels` key). ~700 LOC gone, one show-affecting bug fixed, zero design decisions. Half a day. +2. **Extract `settings.py` + `camera_feed.py` + `monitor.py` from `app.py`; make `body_grid_tester.py` and `shader_app.py` import them.** Removes §3a–3e (≈230 LOC), fixes the `from app import _camera_thread` cross-module private import, makes the defaults agree with the file. One day. +3. **Collapse `shader_app`/`playlist_app`**: `playlist_app --setup`, delete `Rotation` + `shader_app.main` + `setups.json`, rename the remainder `shader_stage.py`, split out `video_player.py`. One show entry point for shaders instead of two, one Setup list instead of two. Half a day + doc update. +4. **`shader_test_rig.py` (or `conftest.py`) + public `Renderer.read_buffer()`**; move each Setup's tests into its own folder; prune layout-probing tests to spec-level behaviour. −350 LOC boilerplate immediately, and the only way the Setup test suite stays maintainable as Setups are added. One to two days. +5. **Split `app.main()` into a `Kiosk` class with per-state tick methods, and `bodycaptcha_editor.py` into model/widgets/views; share `levels.py` between game and editor.** Biggest readability win for small-context readers (756- and 847-LOC files → ≤300 each), enables the first real tests of the show state machine. Two to three days; do after 1–2 so the file is already smaller. + +Docs: after #1–3, rewrite `docs/FundingCAPTCHA.md` §Monitoring, §Settings (`.local` is gitignored; drop `keepaway_body`; drop `/captcha/mode`), §Games, §Body Grid tester; mark ADR-0012 "Superseded by ADR-0013"; add "Amended by ADR-0022" to ADR-0013's blob-detection paragraph; delete `docs/games/rhythm.md`/`upsidedown.md` (or convert to issues); fix `CONTEXT.md:76`; fix `AGENTS.md` "Node tests (FundingCAPTCHA)". diff --git a/docs/audit/2026-11-09/detail/showcontrol-core.md b/docs/audit/2026-11-09/detail/showcontrol-core.md new file mode 100644 index 0000000..7e65448 --- /dev/null +++ b/docs/audit/2026-11-09/detail/showcontrol-core.md @@ -0,0 +1,780 @@ +# Audit — ShowControl core (IIVision, FlowerBeds, TreeHouse, Dashboard, OSCFabric, scripts, CI) + +Read-only audit, 2026-11-09. Scope excludes FundingCAPTCHA and PlayingThePipes except where they +import in-scope code. All paths relative to `the repo root`. Git facts come from the +full 207-commit history (2026-01-24 → 2026-11-08); the May 2026 spike (108 commits) was the +pre-show crunch, after which in-scope Python has barely moved (TreeHouse: 3 commits since June; +FlowerBeds/Dashboard/OSCFabric/IIVision-core: zero functional commits since 2026-05-29). + +Test baseline: `python3 -m pytest IIVision ShowControl/FlowerBeds ShowControl/TreeHouse +ShowControl/OSCFabric scripts` → **116 passed** (2.5 s). `node --test` at repo root → **0 tests**. + +--- + +## 0. Headline findings (blunt) + +1. **The TreeHouse Python LED pipeline has no consumer in the current show.** ESP32-S3 controllers + (ADR-0020/0021) ignore Garden State and animate standalone; three *additional* standalone Pico + firmwares exist (`Firmware/Club_Pico`, `HouseSwarming_Pico`, `Fungi_Pico`, all docstring'd + "stopgap", 2026-05-29). Meanwhile `coordinator.load_config` reads `raw.get("pico")` + (`coordinator.py:177`) but `settings.json` only has `pico_dioramas`/`pico_structure` + (`settings.json:2-10`), so the one `PicoDriver` opens `/dev/ttyACM0` by default — and + `dormer.pico_pin: 0`, `porch_lights.pico_pin: 1` collide with `dioramas[0..1].pico_pin` on + that single serial link. ≈650 LOC (`displays/led.py`, `effect.py`, `porch_lights.py`, + `pico_driver.py`, `LEDControllable`/`ChannelFrame`, half of `coordinator.py`, + `test_forge_and_flora.py`, Dashboard LED-pattern UI) is live-but-inert. Needs an explicit + keep/kill decision; ADR-0020 already promised removal "once hardware is installed". +2. **Two Dashboard→TreeHouse controls are broken at the call site.** + `visualizer.py:112` calls `_coordinator.set_forge_mode(...)` — no such method exists on + `Coordinator` (AttributeError → 500). `Dashboard/treehouse.html:45-47` sends show modes + `full`/`dim`/`off`; `ShowMode` is `active`/`dim`/`inactive` (`displays/base.py:10-13`, + renamed by ADR-0007), so two of three buttons log "Unknown show mode". +3. **FlowerBeds has no OSC listener at all** (no `Dispatcher`/`osc_server` in + `ShowControl/FlowerBeds/*.py`). `docs/FlowerBeds.md:103` documents `/flowerbeds/mode` as + inbound; ADR-0007 requires it; `Dashboard/serve.py:78` relays it into the void. +4. **IIVision ships ~140 LOC of dead camera code.** `OrbbecRGBCamera` + `RGBFrame` + (`camera.py:38-53, 303-425`) existed only for the ArUco layout calibrator deleted by ADR-0015; + nothing imports them; `cv2` (needed at `camera.py:409-412`) is not in any requirements file. +5. **AGENTS.md constraint #1 names a function that does not exist.** `IIVision.Blob3D.world_pos_cm()` + — `Blob3D` (`blob_tracker.py:76-83`) has no such method; the conversion is + `IIVision.transforms.orbbec_to_world()` (`transforms.py:62-73`). Repeated in + `docs/agents/python-conventions.md:28`. +6. **AGENTS.md constraint #3 / `docs/agents/firmware.md:9` claim FlowerBeds firmware reads a + generated `config.h`.** It does not: `Firmware/FlowerBeds_Follow_ServoController/*.ino:26` + hardcodes `IPAddress ip(192,168,1,51)`, and `scripts/hooks/firmware_config_gen.py:20` says + `TODO: generate ... config.h too`. Also `flower_beds.py:129-131` `ControllerConfig` hardcodes + `"192.168.1.50"` (and is unused). +7. **`make test` is misdocumented.** AGENTS.md/running-and-testing.md say it "runs Node tests + (FundingCAPTCHA)"; there are no `*.test.js` files or `package.json` anywhere — `node --test` + reports `# tests 0`. Only JS in the repo is `FlowerBeds/layout_tool/layout.js`. +8. **No packaging anywhere** (no `pyproject.toml`/`setup.py`/`conftest.py`). Elements reach + IIVision/OSCFabric via `sys.path.insert` (`FlowerBeds/main.py:32,53`) and tests only work via + `python -m pytest` from repo root (cwd on `sys.path`). TreeHouse and Dashboard don't use + OSCFabric at all — they re-implement `network.json` loading. + +--- + +## 1. Inventory + +Legend — **P** production show path, **O** operator tool, **D** dev/test only, **X** prototype / +abandoned / dead. LOC = `wc -l`. "Importers" = files outside the file's own directory that import it +(grep across whole repo). + +### IIVision/ (1,582 LOC) + +| File | LOC | Purpose | Importers | Class | +|---|---|---|---|---| +| `__init__.py` | 47 | Re-exports public API (incl. dead `OrbbecRGBCamera`, `RGBFrame`) | FlowerBeds/main.py, FundingCAPTCHA app.py/shader_app.py/body_grid_tester.py/silhouette.py | P | +| `blob_tracker.py` | 434 | `Calibration`, `Calibrator`, `BlobTracker`, `FramePacket`, `DetectionConfig`, `Blob2D/3D` | pipeline.py, camera.py, silhouette_warp.py, FundingCAPTCHA (`detect_foreground`, `threshold_map`, `CameraIntrinsics`) | P | +| `blob_stabilizer.py` | 196 | `BlobStabilizer`, `StabilizerConfig`, `TrackedBlob`, `TrackState` | pipeline.py, FlowerBeds flower_beds.py/test, FundingCAPTCHA body_blobs.py | P | +| `camera.py` | 425 | `OrbbecCamera`, `MockCamera`, **dead** `OrbbecRGBCamera`/`RGBFrame` | FlowerBeds main.py, FundingCAPTCHA ×3 | P (lines 303-425: X) | +| `transforms.py` | 92 | `Rotator`, `Transform`, `rotator_to_matrix`, `transform_position`, `orbbec_to_world`, `look_yaw_degrees` | pipeline.py, silhouette_warp.py, FlowerBeds flower_beds.py/main.py | P | +| `pipeline.py` | 108 | `build_calibration`, `run_pipeline`, `filter_positions_near_points` | FlowerBeds main.py, flower_beds.py | P | +| `silhouette_warp.py` | 80 | `reproject_silhouette` (FundingCAPTCHA only) | FundingCAPTCHA silhouette.py | P (not in architecture.md table) | +| `test_blob_stabilizer.py` | 45 | `update_tracks()` tests | — | D | +| `test_detect_foreground.py` | 155 | `detect_foreground()` tests | — | D | + +No tests cover `Calibrator.build`, `BlobTracker.detect`, `_compute_3d_blobs`, `transforms.py`, +or `pipeline.run_pipeline`. + +### ShowControl/FlowerBeds/ (3,652 LOC incl. 848 settings.json) + +| File | LOC | Purpose | Importers | Class | +|---|---|---|---|---| +| `main.py` | 454 | CLI, config load, calibration persistence, frame loop, two motor-calibration modes | — (entry) | P | +| `flower_beds.py` | 379 | Config dataclasses, `Attraction`, `FlowerCluster`, `FlowerModule`, `Coordinator`, `MotorCommand` | main.py, test_flower_beds.py | P | +| `visualizer.py` | 434 | FastAPI WS server + 240-line embedded HTML/JS client; `broadcast()`, `/logs` | main.py (lazy), flower_beds.py (TYPE_CHECKING only) | P | +| `diag.py` | 73 | `/cg/ff/ping`→`/cg/ff/pong` client, `parse_found_ids` | main.py, layout_tool.py, test_diag.py | P/O | +| `layout_tool.py` | 246 | FastAPI server for operator layout GUI (port 8764) | test_layout_tool.py | O | +| `layout_tool/index.html` | 48 | GUI shell | layout_tool.py (served) | O | +| `layout_tool/style.css` | 42 | GUI styles | — | O | +| `layout_tool/layout.js` | 569 | Canvas drag/aim/save client | index.html | O | +| `settings.json` | 848 | 12 modules × 4 clusters, cameras, stabilizer, attraction | main.py, layout_tool.py | P | +| `requirements.txt` | 9 | numpy scipy python-osc fastapi uvicorn **aiofiles** websockets pyorbbecsdk2 | — | P (`aiofiles` imported nowhere) | +| `deploy/flowerbeds.service` | 22 | systemd unit | deploy/install.sh | P | +| `deploy/install.sh` | 40 | apt numpy/scipy + pip + unit install | scripts/deploy.sh, install-services.sh | P | +| `test_flower_beds.py` | 140 | yaw clamp / module yaw tests | — | D | +| `test_diag.py` | 88 | ping/pong parse tests | — | D | +| `test_layout_tool.py` | 260 | FastAPI TestClient tests | — | D | + +### ShowControl/TreeHouse/ (3,793 LOC incl. 549 club_messages.txt) + +| File | LOC | Purpose | Importers | Class | +|---|---|---|---|---| +| `main.py` | 219 | CLI, asyncio supervisor (weston, 2 renderers), frame loop | — (entry) | P | +| `coordinator.py` | 485 | 9 config dataclasses + `load_config` (reads settings+network.json) + `build_displays` + `Coordinator` (GardenState, stale-sender, branch math) | main.py, visualizer.py (TYPE), osc_server.py (TYPE), 3 tests | P | +| `osc_server.py` | 78 | python-osc dispatcher → Coordinator setters | main.py | P | +| `location_sender.py` | 158 | Garden State → ESP32 controllers, change+heartbeat gating | main.py, coordinator.py, test | P (receivers ignore it per ADR-0021) | +| `visualizer.py` | 199 | FastAPI WS (`/ws`, `/logs`) + 7 `/control/*` endpoints | main.py | P | +| `pico_driver.py` | 55 | JSON-lines pixel sink over USB serial | main.py | **P-inert** (see §2) | +| `branch_controller.py` | 49 | JSON-lines servo commands over USB serial | main.py | P | +| `displays/__init__.py` | 24 | re-exports | coordinator, main, tests | P | +| `displays/base.py` | 78 | `ShowMode`, `Color`, `ChannelFrame`, `GardenState`, `Controllable`, `LEDControllable` | all TreeHouse modules | P | +| `displays/led.py` | 119 | generic `LEDDisplay` (6 patterns; ignores GardenState) | coordinator.py | P-inert | +| `displays/effect.py` | 133 | `ForgeAndFloraDisplay` two-strip crossfade | coordinator.py, test | P-inert | +| `displays/video.py` | 87 | `LookingGlassDisplay` → OSC to renderer every frame | coordinator.py | P | +| `displays/porch_lights.py` | 108 | blow-up/aftermath state machine | coordinator.py | P-inert | +| `looking_glass/renderer.py` | 140 | pygame+moderngl shader subprocess, OSC :9002 | main.py (subprocess) | P | +| `looking_glass/{bloom,fractal,mycelium,cosmos}.glsl` | 31/35/72/59 | shaders | renderer.py | P | +| `looking_glass/__init__.py` | 0 | enables `python -m looking_glass.renderer` | — | P | +| `looking_glass/deploy/looking_glass.service` | 26 | **self-declared OBSOLETE** (ADR-0018) | — | X | +| `looking_glass/deploy/install.sh` | 33 | duplicate of `deploy/install.sh` apt/pip steps | — | X | +| `club_screen.py` | 281 | pygame text-rotation subprocess (ADR-0017) | main.py (subprocess) | P | +| `club_messages.txt` | 549 | message content | club_screen.py | P | +| `settings.json` | 126 | see §6 — ~25 lines unread | coordinator.py, club_screen.py | P | +| `requirements.txt` | 7 | python-osc pyserial moderngl numpy uvicorn fastapi pygame | — | P (`numpy` imported nowhere in TreeHouse) | +| `deploy/treehouse.service` | 29 | systemd unit (weston env) | deploy/install.sh | P | +| `deploy/install.sh` | 52 | apt weston/seatd + pip + unit | scripts | P | +| `deploy/weston.ini` | 17 | dual-output compositor config | main.py | P | +| `test_branch_positions.py` | 76 | branch math | — | D | +| `test_dead_sender.py` | 163 | stale-sender zeroing (ADR-0007) | — | D (valid, tests live code) | +| `test_forge_and_flora.py` | 114 | F&F reactivity | — | D (tests inert code) | +| `test_location_sender.py` | 191 | sender gating + config resolution | — | D | + +### ShowControl/Dashboard/ (1,019 LOC) + +| File | LOC | Purpose | Class | +|---|---|---|---| +| `serve.py` | 117 | FastAPI static + `POST /api/mode` OSC relay | P | +| `index.html` | 113 | 4 status cards polling `/health` on hardcoded ports 8765/8766/8080/8767 (`index.html:55-60` — duplicates `network.json` `http_port`) | P | +| `treehouse.html` | 255 | WS state + log view + control buttons (2 broken, see §0) | P | +| `flowerbeds.html` / `fundingcaptcha.html` | 59/59 | iframe + `/logs` WS — near-identical | P | +| `playingthepipes.html` | 21 | stub | P | +| `dashboard.css` | 332 | styles | P | +| `deploy/dashboard.service`, `deploy/install.sh` | 18/38 | unit + installer | P | +| `requirements.txt` | 7 | fastapi uvicorn python-osc pydantic | P | + +Note: no page in the Dashboard actually calls `POST /api/mode` (grep `api/mode` in `*.html` → none). +The only mode control in the UI is `treehouse.html`'s direct POST to the TreeHouse visualizer +(`/control/show/mode`), with the wrong vocabulary. `serve.py`'s relay endpoint is reachable only +by curl. + +### ShowControl/OSCFabric/ (314 LOC) + +| File | LOC | Purpose | Importers | Class | +|---|---|---|---|---| +| `__init__.py` | 4 | exports | FlowerBeds main.py, FundingCAPTCHA app.py | P | +| `config.py` | 20 | `load_network_config()` | same | P | +| `client.py` | 76 | `FabricClient` (change+heartbeat, one target) | same | P | +| `test_client.py` | 214 | **pytest tests** (20 tests). Despite the name it is not a manual tool. Tests `send_event("game_started")` which no Element sends. | — | D | + +### ShowControl root + +`network.json` (27 LOC) — P. Keys: `_comment`, `lan`, `heartbeat_interval_s`, `change_epsilon`, +`elements{4}`, `firmware{8}`. No other root-level files. + +### scripts/ (617 LOC), Makefile, pytest.ini, .github/ + +| File | LOC | Purpose | Class | +|---|---|---|---| +| `scripts/deploy.sh` | 79 | laptop → show machine push + `install.sh` | O | +| `scripts/bootstrap-deploy.sh` | 53 | one-time receive.denyCurrentBranch + sudoers | O | +| `scripts/install-services.sh` | 63 | loops per-element `deploy/install.sh` | O | +| `scripts/install-git-hooks.sh` | 20 | symlinks `scripts/git-hooks/*` | D | +| `scripts/git-hooks/pre-commit` | 120 | private-content guard | D | +| `scripts/test_git_hooks.py` | 149 | tests for pre-commit | D | +| `scripts/hooks/smoke_test.py` | 23 | **stub that exits 0**; referenced only by `architecture.md:23`; no `.claude/settings.json` in repo wires it | X | +| `scripts/hooks/firmware_config_gen.py` | 110 | network.json → `TreeHouse_Controllers/include/net_config.h`; FlowerBeds `config.h` is a TODO | O | +| `Makefile` | 5 | `node --test` (0 tests) + `python3 -m pytest \|\| test $? -eq 5` | D | +| `pytest.ini` | 2 | `testpaths = ShowControl IIVision scripts` | D | +| `requirements-dev.txt` | 9 | pytest numpy scipy python-osc fastapi httpx uvicorn pygame moderngl (no `pyserial` — TreeHouse tests avoid importing `main.py`) | D | +| `.github/workflows/tests.yml` | 74 | PR-only: setup node/python, pip dev reqs, apt headless GL, private-content diff guard, `make test` | D | +| `.github/workflows/claude.yml` | 50 | `@claude` mention bot | D | + +--- + +## 2. Dead code + +### Confirmed dead (nothing references it) + +| Item | Location | LOC | Evidence | +|---|---|---|---| +| `OrbbecRGBCamera`, `RGBFrame`, `cv2` import, section headers | `IIVision/camera.py:30-33, 38-53, 303-425` | ~140 | grep `OrbbecRGBCamera\|RGBFrame` outside IIVision → 0 hits. Comment at `camera.py:39,304` says "used exclusively by the layout calibrator" — deleted by ADR-0015. `cv2` not in any requirements. Also exported from `__init__.py:18,34-35`. | +| Unused imports | `camera.py:19` (`struct`), `:23` (`contextmanager`), `:91` (`OBAlignMode`); `FlowerBeds/main.py:39-48` (`Attraction`, `ClusterConfig`, `ControllerConfig`, `ModuleConfig`); `FlowerBeds/visualizer.py:18` (`Any`); `TreeHouse/pico_driver.py:6` (`Color`); `Dashboard/serve.py:14` (`sys`); tests: `test_layout_tool.py:5-6`, `test_branch_positions.py:2`, `test_detect_foreground.py:5` | ~12 | pyflakes | +| `ControllerConfig` | `FlowerBeds/flower_beds.py:128-135` | 8 | imported by main.py but never instantiated; hardcodes `192.168.1.50:9000` (constraint #3 violation) | +| `_get_loop()` | `FlowerBeds/visualizer.py:377-382` | 6 | 0 callers; pyflakes also flags useless `global _loop` at :378 and :415 | +| `calibration_state` | `FlowerBeds/main.py:222-223, 274` | 3 | always the literal `"calibrated"`; `docs/FlowerBeds.md:146` tells operators to check it | +| `TrackState`/`PreStabilizeFilter` exports | `IIVision/__init__.py:15-16, 26, 30` | 2 | `TrackState` used only inside IIVision + its test; `PreStabilizeFilter` alias unused outside (flower_beds.py:353 redefines the Callable) | +| `Blob3D.cam_half_extents_m`, `.median_z_m`, `.sample_count` | `IIVision/blob_tracker.py:81-83, 429-431` | 6 | computed, never read anywhere | +| `Calibration.delete()` | `blob_tracker.py:138-141` | 4 | only mention is the module docstring at `pipeline.py:16` | +| `CalibrationConfig` | `blob_tracker.py:47-49` | 3 | only ever default-constructed (`:158`); no caller passes one | +| `looking_glass/deploy/looking_glass.service` | 26 | self-described OBSOLETE | git history is the "reference" | +| `looking_glass/deploy/install.sh` | 33 | duplicates `deploy/install.sh:18-30`; adds `usermod -aG _seat` that the live installer lacks (see §8) | +| `scripts/hooks/smoke_test.py` | 23 | stub; `_ = data.get(...)`; nothing invokes it | | +| `.gitignore:17` `layout_calibrated.json` | 1 | file removed by ADR-0015 | | +| `/control/forge-mode` endpoint | `TreeHouse/visualizer.py:108-113` + `Dashboard/treehouse.html:64-69` | ~15 | calls nonexistent `Coordinator.set_forge_mode`; and even `ForgeAndFloraDisplay.set_mode/set_blend` (`effect.py:51-59`) are overwritten every frame by `update()` at `effect.py:79`, so the whole control path is a no-op by design since ADR-0008 | +| `ForgeAndFloraDisplay.set_mode`, `set_blend`, `set_transition_speed`; `LookingGlassDisplay.set_speed`, `set_mirror_depth`; `LEDDisplay.set_color`, `set_brightness`; `Controllable.enable/disable` (only `disable()` called, in a test) | `effect.py:51-62`, `video.py:57-61`, `led.py:49-53`, `base.py:58-62` | ~30 | no callers | +| `Coordinator.get(name)` | `coordinator.py:412-413` | 2 | no callers | +| FlowerBeds `requirements.txt:6` `aiofiles` | 1 | imported nowhere in repo | +| TreeHouse `requirements.txt` `numpy` | 1 | not imported in TreeHouse (ADR-0018 explicitly removed it from the renderer) | + +### Config keys nothing reads — see §6 (≈25 lines in TreeHouse/settings.json). + +### Live-but-inert: the TreeHouse Pico/Python LED path (decision required) + +Evidence the path is superseded in practice, not just on paper: +- ADR-0020 (Sep 2026): "Pi Pico path … stays in the tree until the ESP32-S3 hardware is + physically installed, then is removed along with `pico_driver.py` and the `pico_*` blocks". +- ADR-0021 (Oct 2026): five ESP32 controllers built incl. Garage; they *ignore* Garden State. +- `Firmware/Club_Pico`, `HouseSwarming_Pico`, `Fungi_Pico` (2026-05-29) are each "standalone Pico + light controller (stopgap) … no host" — i.e. the dioramas were lit at the show *without* the + Pi pipeline. +- The Pi code can't drive the documented hardware anyway: single `PicoDriver` + (`main.py:149`), config key mismatch (`coordinator.py:177` reads `pico`; settings has + `pico_dioramas`/`pico_structure`), pin collisions (settings `dioramas[0].pico_pin=0` vs + `dormer.pico_pin=0`; `dioramas[1]=1` vs `porch_lights=1`). +- `displays/led.py:60-63` `LEDDisplay.update()` ignores `state` entirely — ADR-0008's + `GardenStateResponse` was never built. Only `ForgeAndFloraDisplay` and `PorchLightsDisplay` + react to GardenState. + +If killed: `pico_driver.py` (55), `displays/led.py` (119), `displays/effect.py` (133), +`displays/porch_lights.py` (108), `LEDControllable`/`ChannelFrame`/`Color`/`scale_color` in +`base.py` (~25), `DioramaConfig`/`DormerConfig`/F&F/porch config + loading + `build_displays` +LED branches in `coordinator.py` (~130), `get_all_frames`/`_led_displays`/`set_display_pattern`/ +`reset_porch_lights`/`_porch_lights` (~25), `test_forge_and_flora.py` (114), Dashboard LED/forge/ +porch controls in `treehouse.html` (~60 + `visualizer.py` endpoints ~30), `Firmware/TreeHouse_PicoLEDs`, +settings blocks (~70 lines). **≈ 800 LOC Python/JS + firmware.** Keep `GardenState`, +`Controllable`, `LookingGlassDisplay`, branch logic, `LocationSender`. + +If kept: fix the three bugs above, implement two drivers, and say so in ADR-0020. + +### "Display" → "Controllable" rename status (CONTEXT.md:139,143 says "old name — being replaced") + +Half-done since 2026-05-22. Base classes are `Controllable`/`LEDControllable` (`base.py:51,73`), +but every concrete class is still `*Display` (`LEDDisplay`, `ForgeAndFloraDisplay`, +`LookingGlassDisplay`, `PorchLightsDisplay`), the package is `displays/`, and `Coordinator` uses +`_displays`, `display_names`, `set_display_pattern`, `build_displays`. ADR-0006's "each room becomes +its own class (`HouseSwarmingDisplay`, `ClubDisplay`, `AtticDisplay`)" never happened — rooms are +rows in `settings.json.dioramas` fed to one generic `LEDDisplay`. ADR-0006 also says +`BranchController` inherits `Controllable`; it doesn't (`branch_controller.py:9` is a plain serial +driver; branch math lives in `Coordinator._update_branch_positions`, `coordinator.py:453-467`). + +### `club_screen.py` +Live (subprocess from `main.py:193`), but reads `settings.json.club_screen.{font_size_max, +font_size_min, hold_top_seconds, scroll_speed_px_per_sec, hold_bottom_seconds, +fade_duration_seconds, messages_file}` (`club_screen.py:166-171,243`) while `settings.json:104-107` +provides `interval_seconds` (unread) and `messages_file` only. ADR-0017's "configurable timer +(`interval_seconds`)" was replaced by a fade/hold/scroll state machine without updating ADR or +settings. Also hardcodes `WIDTH, HEIGHT = 800, 480` and `_load_config()` ignores `--config` +(always `./settings.json`). + +### `location_sender.py` / `test_dead_sender.py` +Both live and tested. `LocationSender` is the only current Pi→lights link (receivers ignore it, +ADR-0021). `test_dead_sender.py` tests `Coordinator._expire_stale_senders` — live code, but it +asserts on private attrs (`c._flowerbeds_activity`). Note `LocationSender._should_send_float` +(`location_sender.py:129-148`) re-implements `FabricClient.report/tick` semantics +(`OSCFabric/client.py:39-56`) — duplication, see §3. + +### `IIVision/pipeline.py`, `transforms.py` +All public functions are called (`filter_positions_near_points` ← flower_beds.py:16; +`build_calibration`/`run_pipeline` ← main.py; all five transforms used). No dead functions here. + +### OSCFabric `test_client.py` +It is a pytest module (20 tests, pytest fixtures, `SpyTransport`). Not a manual tool. Its +docstring (`:4`) still says "Red-green-refactor" which is a process note, not documentation. + +--- + +## 3. Duplication + +### 3a. FastAPI WebSocket broadcast + log streaming (FlowerBeds vs TreeHouse) + +Byte-for-byte duplicates: +- `WebSocketLogHandler`: `FlowerBeds/visualizer.py:319-334` ≡ `TreeHouse/visualizer.py:45-60` +- `/ws` endpoint: `FlowerBeds/visualizer.py:347-358` ≡ `TreeHouse/visualizer.py:130-140` +- `/logs` endpoint: `FlowerBeds/visualizer.py:361-374` ≡ `TreeHouse/visualizer.py:143-156` +- `/health`: `FlowerBeds/visualizer.py:342-344` ≡ `TreeHouse/visualizer.py:67-69` ≡ `Dashboard/serve.py:49-51` +- module globals `app`, `CORSMiddleware(allow_origins=["*"])`, `_connections`, `_log_queues`, + `_loop`: `FlowerBeds/visualizer.py:312-316` ≡ `TreeHouse/visualizer.py:32-37` +- fan-out loop with dead-socket pruning: `FlowerBeds/visualizer.py:393-404` ≡ `TreeHouse/visualizer.py:174-184` +- uvicorn.Config(..., log_level="warning", loop="asyncio"): `FlowerBeds:424`, `TreeHouse:195`, `layout_tool.py:242`, `serve.py:113` + +Differences are only threading model (FlowerBeds runs uvicorn in a daemon thread and bridges via +`run_coroutine_threadsafe`; TreeHouse is already asyncio) and the logger name. + +Dashboard HTML duplicates: `flowerbeds.html` ≡ `fundingcaptcha.html` except iframe URL/port +(`:28-30`), and the `appendLog`/`connectLogs` JS block (`flowerbeds.html:36-56`) is repeated in +`treehouse.html:226-250`, `fundingcaptcha.html`. + +**Proposal:** `ShowControl/ShowWeb/` package (name chosen to sit beside `OSCFabric`): +``` +ShowControl/ShowWeb/__init__.py +ShowControl/ShowWeb/live.py + class LiveServer: + def __init__(self, logger_name: str, port: int, html: str | Path | None = None, + extra_routes: Callable[[FastAPI], None] | None = None) + def start_in_thread(self) -> None # FlowerBeds (sync frame loop) + async def serve(self) -> None # TreeHouse (asyncio frame loop) + def broadcast(self, payload: dict) -> None # thread-safe; no-op when no clients + async def broadcast_async(self, payload: dict) -> None + # provides /health, /ws, /logs, optional GET / ; attaches WebSocketLogHandler to logger_name +ShowControl/Dashboard/log-panel.js # shared appendLog/connectLogs; pages