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, ISOcreated_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.jsfilled: 💾 save = name prompt + POSTcamPath.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 viawrite-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_capsuleper the frozen contract: copiesfrontend/dist(clear "runcd frontend && npm run build" error if missing), copies the manifest-referenced media tomedia/, bakes every read-only API response extension-less at the live API's relative paths (api/manifestwithcapsule: true+has_capture: false,api/events,api/anchors,api/videos/{id}/poses,api/beats/api/pointcloud/api/splatwhen present,api/audio/{id}via the same lazyingest.extract_audio_hqcache the live route uses), injects<script>window.__F4D_API_BASE__=""</script>ahead of the app<script>, ships stdlibserve.py(HTTP/1.1, 206 Range slicing incl.bytes=N-and suffixbytes=-N, 416 past-EOF,Accept-Ranges, JSON content types for the extension-lessapi/*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 atapi/paths/index.html— a filesystem can't holdpathsas both file and dir; the stock 301 →/api/paths/→ index.html keepsfetch("/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 throughapi.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 withContent-Range: bytes 0-100/{size}and 101 body bytes (mp4ftypmagic verified), open-ended + suffix ranges, 416, audio 200audio/mp4+ ranged, 404s.- End-to-end proof WITHOUT a browser (evidence below).
Quality bar
uv run pytest backend/tests -p no:warnings→ 169 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):
- Capsule acceptance proper:
python serve.pyin 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-uicontrols absent (body.capsule), saved-path dropdown loads a baked path. - 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.
- 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.