festifun/plan/status/lane-G.md
type-two 4bc2638c12 lane G: status — M16+M17 complete, 169 passed, capsule E2E evidence, CR-4 noted
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:51:33 +10:00

6.7 KiB

Status — lane G (persistence & capsule, branch lane/g-capsule)

Round 1 — 2026-07-17 — STATUS: ready_to_merge

Directives acknowledged: Round 4 of plan/DIRECTIVES.md (phase 5 begins; foundation2 merged; worktree rule followed — this lane runs in its own git worktree).

Acceptance checklist

M16 — server-side camera paths:

  • Routes implemented — by foundation2 (per the phase-5 ownership table); lane G deepened coverage only, per scope.
  • backend/tests/test_paths.py — 30 tests: roundtrip deep-equal incl. float precision + a 500-keyframe path, multiple paths coexist / id-ordered listing / summary-only shape, duplicate + unicode + whitespace + 300-char names verbatim, ISO created_at, empty-keyframes path valid (director degrade must be saveable), extra JSON keys ride along, get/delete 404s, delete idempotence, an 18-case parametrized 422 matrix over the frozen camPath contract, 422 creates no row, missing/aliased request fields 422. Cleans up its rows (test_phase5_api runs later and asserts an empty table).
  • frontend/src/pathsStore.js filled: 💾 save = name prompt + POST camPath.toJSON(); dropdown load = GET /api/paths/{id} + camPath.fromJSON(data.json); 🗑 delete with confirm; dropdown refreshed after every mutation (new save left selected); controls unhidden + ready = true. Capsule mode: save/del hide via write-ui; the load dropdown stays (paths are baked read-only — below) and hides itself if the listing fetch fails.
  • Live save → reload page → load reproduces keyframes exactly — NEEDS LIVE BROWSER (list for coordinator below). The exact-roundtrip property is locked server-side by test_roundtrip_preserves_float_precision / test_roundtrip_large_keyframe_list.

M17 — memory capsule:

  • backend/festival4d/capsule.py::build_capsule per the frozen contract: copies frontend/dist (clear "run cd frontend && npm run build" error if missing), copies the manifest-referenced media to media/, bakes every read-only API response extension-less at the live API's relative paths (api/manifest with capsule: true + has_capture: false, api/events, api/anchors, api/videos/{id}/poses, api/beats/api/pointcloud/ api/splat when present, api/audio/{id} via the same lazy ingest.extract_audio_hq cache the live route uses), injects <script>window.__F4D_API_BASE__=""</script> ahead of the app <script>, ships stdlib serve.py (HTTP/1.1, 206 Range slicing incl. bytes=N- and suffix bytes=-N, 416 past-EOF, Accept-Ranges, JSON content types for the extension-less api/* files). Optional artifacts degrade by omission; refuses to clobber a non-capsule output dir; rebuild over a previous capsule is idempotent (no double-inject). Bonus within scope: M16 saved paths are baked read-only (api/paths/{id}, listing at api/paths/index.html — a filesystem can't hold paths as both file and dir; the stock 301 → /api/paths/ → index.html keeps fetch("/api/paths") working unchanged).
  • backend/tests/test_capsule.py — 12 tests: bundle structure; baked JSON deep-equal vs the live route functions (shape drift impossible — capsule bakes through api.py); manifest key-set == live route, urls resolve inside the bundle; injection present exactly once and before the module script; degrade-when-absent; no-dist error; overwrite guard; CLI dispatch (cli.main(["capsule","--out",…]) == 0, hermetic via monkeypatched REPO_ROOT); serve.py exercised over real HTTP in a subprocess — 200 + Accept-Ranges, bytes=0-100 → 206 with Content-Range: bytes 0-100/{size} and 101 body bytes (mp4 ftyp magic verified), open-ended + suffix ranges, 416, audio 200 audio/mp4 + ranged, 404s.
  • End-to-end proof WITHOUT a browser (evidence below).

Quality bar

  • uv run pytest backend/tests -p no:warnings169 passed (floor 127 + 30 paths + 12 capsule), zero failures.
  • cd frontend && npm run build → clean, only the pre-existing >500 kB chunk-size warning.

E2E capsule evidence (exact commands, run in the worktree):

$ cd frontend && npm run build                      # ✓ built in 860ms
$ uv run python -m festival4d synthetic             # 3 videos, 3106 points, 7 events, 4 anchors
$ uv run python -m festival4d capsule
  capsule: baked 18 files (6826705 bytes) at <worktree>/data/capsule
$ cd data/capsule && python3 serve.py --port 8901
$ curl -s http://127.0.0.1:8901/ | grep -o 'window.__F4D_API_BASE__=""'
  window.__F4D_API_BASE__=""
$ curl -si http://127.0.0.1:8901/api/manifest
  HTTP/1.1 200 OK / Content-Type: application/json
  capsule: True | videos: 3 | t_global_max: 21.37 | has_poses: True
$ curl -si -H "Range: bytes=0-100" http://127.0.0.1:8901/media/cam0.mp4
  HTTP/1.1 206 Partial Content / Accept-Ranges: bytes / Content-Length: 101
  Content-Range: bytes 0-100/1533609
$ curl -s -H "Range: bytes=500000-500099" .../media/cam0.mp4   # mid-file seek
  206, received 100B
$ curl -s http://127.0.0.1:8901/api/audio/1
  200, audio/mp4, 440201 bytes, magic `ftypM4A`
$ curl -sL http://127.0.0.1:8901/api/paths        # []  final:200 application/json
$ curl .../api/pointcloud → 200 46800B; .../api/beats → 404 (degrade: no analysis run)

Blockers / change requests: none blocking. CR-4 filed (plan/CHANGE_REQUESTS.md): test_phase5_api.py::test_cli_dispatches_stubs_gracefully asserted the stub exit-2 contract for capsule; with M17 landed that assertion fails by design (CR-1 precedent). Applied the minimal edit (removed only ["capsule"]; features/direct untouched for lane E) — integration2 to ratify. Capsule CLI dispatch is now covered for real in test_capsule.py.

Small note for integration2 (frozen file, not edited): .gitignore covers data/raw/ + data/work/ but not data/capsule/ or data/project.db, so a default capsule build leaves a ~7 MB untracked tree; consider ignoring data/ wholesale (or adding the two entries).

Needs live-browser verification (for coordinator / integration2):

  1. Capsule acceptance proper: python serve.py in the bundle, open it focused — app loads with zero requests to :8000 (network tab), videos play and seek, 3D scene + timeline work, 🎧 spatial audio works, all .write-ui controls absent (body.capsule), saved-path dropdown loads a baked path.
  2. M16 live flow: save a path (💾, name prompt) → reload page → load from dropdown → camera lands on the exact keyframes; delete removes it from the dropdown.
  3. pathsStore dropdown behavior with an older capsule (no baked paths): dropdown hides itself.

Next: hand to coordinator for merge; not merging to main per lane instructions this round.