diff --git a/.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md b/.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md new file mode 100644 index 0000000..9f12518 --- /dev/null +++ b/.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md @@ -0,0 +1,159 @@ +--- +phase: 01-stabilize-video-pipeline +verified: 2026-05-16T09:11:33Z +verifier: gsd-verifier +goal: The video ring buffer captures the most recent 30 s of the active tab's video continuously across tab switches, with a playable WebM header retained — so that on export the assembled `last_30sec.webm` will play. +status: human_needed +axes_total: 10 +axes_passed: 10 +axes_failed: 0 +axes_advisory: 2 +--- + +# Phase 1: Stabilize Video Pipeline — Goal-Backward Verification + +## Phase Goal (verbatim from ROADMAP.md) + +> The video ring buffer captures the most recent 30 s of the active tab's video continuously across tab switches, with a playable WebM header retained — so that on export the assembled `last_30sec.webm` will play. + +## Verdict + +**DELIVERED** — 10 of 10 verification axes PASS against the codebase; phase goal is achieved by the wired implementation, not just by SUMMARY narration. Two **ADVISORY** notes recorded that do not block phase completion. Three **operator-side residue** items remain that THIS verifier cannot validate without a Chrome runtime — these MUST be picked up by Phase 4's SPEC §10 smoke pass. + +Status is `human_needed` because the residue items below require a real Chrome operator session (visual picker UX, tab-switch continuity, SW idle survival) — automated codebase verification is exhausted. + +--- + +## Per-Axis Verification Table + +| # | Axis | Evidence (file:line or command) | Verdict | +| -- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| 1 | "30 s ring buffer" — constants + rotation | `src/offscreen/recorder.ts:23-29` (`SEGMENT_DURATION_MS=10_000`, `MAX_SEGMENTS=3`, `VIDEO_BUFFER_DURATION_MS = 30_000`); `:86-89` push + shift cap; `:352-355` production rotation evict-oldest | PASS | +| 2 | "Continuous across tab switches" — no re-acquire | `grep -RIn "onActivated\|onUpdated\|re-attach\|reAttach" src/` returns **no matches** (exit 1); `src/offscreen/recorder.ts:46` comment pins same-MediaStream across rotations; `getDisplayMedia` is screen/window-scoped per D-01 | PASS | +| 3 | "Playable WebM" — D-13 per-segment self-contained | `src/offscreen/recorder.ts:252-302` `startNewSegment` constructs fresh MediaRecorder per rotation; `:349-370` `onSegmentStopped` finalizes & rotates; `tests/offscreen/segment-keyframes.test.ts:195-269` GREEN-pins "each retained segment starts with a keyframe" + 30-s window invariant | PASS | +| 4 | "D-12 base64 wire transfer" | `src/shared/binary.ts:42-85` round-trip helpers; `src/shared/types.ts:64-68` `TransferredVideoSegment.data: string` (base64); `src/offscreen/recorder.ts:574` `await blobToBase64`; `src/background/index.ts:188` `base64ToBlob`; `tests/offscreen/port-serialization.test.ts` 9 tests GREEN | PASS | +| 5 | "T-1-04 sender-id check both ends" | offscreen-side: `src/offscreen/recorder.ts:465-467` `isFromOwnExtension`, `:646` enforced on onMessage; SW-side: `src/background/index.ts:83-87` rejects port with mismatched sender, `:514-517` rejects onMessage with mismatched sender | PASS | +| 6 | "SW respawn safety" | `src/background/index.ts:33-35` Promise; `:548-552` `OFFSCREEN_READY` resolves; `:576-595` `hasDocument()` check + early-resolve on detected pre-existing offscreen (CR-03 fix); `:608-613` `indexedDB.deleteDatabase('VideoRecorderDB')` in `onInstalled` | PASS | +| 7 | "Manifest aligned" | `manifest.json:6-13` lists `desktopCapture` + `offscreen`; **no `tabCapture`**; `grep -RIn "tabCapture\|chrome.alarms" src/` returns **no matches** (exit 1); `chrome.runtime.connect` port replaces alarms keepalive (recorder.ts:606-631 + background/index.ts:78-118) | PASS | +| 8 | "Fixture validity" | `tests/fixtures/last_30sec.webm` exists (1 633 459 bytes); `ffprobe -v error -f matroska -i ...` → exit 0, empty stderr; codec=vp9 Profile 0, 1142×1038, bt709 | PASS | +| 9 | "Acceptance tests aligned" | `tests/offscreen/webm-playback.test.ts:94-114` `decodeDryRunStrict` invokes ffmpeg via `spawnSync` and parses stderr for packet-error + ended-prematurely; `npx vitest run` shows both empirical gates execute (988 ms + 914 ms) and PASS | PASS | +| 10 | "Code-review aftermath — no regression" | 7 test files / 40 tests all green; tsc exit 0; type-safety grep clean; `npm run build` exit 0 (60 modules, dist/ produced); sweep commits 08a79a6 (stop-race), 7c91f52 (re-entrance + start-throw + dual-track teardown), 034155b (port-replaced diagnostic) inspected and preserve D-12/D-13 contracts | PASS | + +**Score: 10/10 axes verified.** + +--- + +## Required Artifacts (3-level check) + +| Artifact | Expected | Status | Details | +| --------------------------------------- | ------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/offscreen/recorder.ts` | getDisplayMedia + D-13 lifecycle + port + handshake | VERIFIED | Exists (673 lines, substantive); imported by offscreen/index.html via crxjs; bootstrap() runs on module load; ALL ring-buffer constants, classifier, lifecycle hardening present | +| `src/background/index.ts` | SW with port-based fetch, T-1-04 sender check, hasDocument check | VERIFIED | Exists (618 lines); receives port via onConnect; runs `chrome.offscreen.hasDocument()` on init; CR-03 fix early-resolves `offscreenReady` on detected pre-existing offscreen | +| `src/shared/binary.ts` | D-12 base64 ↔ Blob helpers | VERIFIED | Exists (85 lines); pure, portable (no FileReader); pinned by 9 tests in port-serialization.test.ts | +| `src/shared/types.ts` | `VideoSegment` + `TransferredVideoSegment` wire-format | VERIFIED | `VideoSegment {data: Blob; timestamp}` + `TransferredVideoSegment {data: string; type; timestamp}` both present; `Message` post-IN-05 | +| `manifest.json` | `desktopCapture` + `offscreen` permissions; NO `tabCapture` | VERIFIED | Confirmed line-by-line | +| `tests/offscreen/*.test.ts` (7 files) | codec-check, handshake, port, port-serialization, segment-keyframes, segment-rotation, webm-playback | VERIFIED | All 7 present; ring-buffer.test.ts retired (IN-03) leaving 7 file × 40-test surface | +| `tests/fixtures/last_30sec.webm` | Empirical D-13 regression fixture | VERIFIED | 1 633 459 bytes; vp9 1142×1038; ffprobe + ffmpeg dry-run both exit 0 | + +--- + +## Key Link Verification + +| From | To | Via | Status | Details | +| --------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `offscreen/index.html` | `src/offscreen/recorder.ts` | `