Files
mokosh/.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md

160 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<T = unknown>` 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` | `<script type="module" src="./recorder.ts">` | WIRED | Confirmed at `src/offscreen/index.html:8`; crxjs picks up via rollupOptions.input in vite.config.ts |
| `recorder.ts: encodeAndSendBuffer` | `binary.ts: blobToBase64` | `import { blobToBase64 } from '../shared/binary'` | WIRED | Imported at `recorder.ts:18`; called at `:574` |
| `background/index.ts: getVideoBufferFromOffscreen` | `binary.ts: base64ToBlob` | `import { base64ToBlob, blobToBase64 } from '../shared/binary'` | WIRED | Imported at `background/index.ts:2`; called at `:188`; result wrapped in `VideoSegment[]` returned to `saveArchive``createArchive` → archive `video/last_30sec.webm` entry |
| `recorder.ts: connectPort` | `background/index.ts: onConnect listener` | `chrome.runtime.connect({ name: 'video-keepalive' })` | WIRED | port name match enforced at `background/index.ts:80-82`; sender-id at `:83-87` |
| `recorder.ts: bootstrap → sendMessage` | `background/index.ts: OFFSCREEN_READY case` | `chrome.runtime.sendMessage({ type: 'OFFSCREEN_READY' })` | WIRED | resolves `offscreenReady` Promise at `background/index.ts:548-552` |
| `background/index.ts: startVideoCapture` | `recorder.ts: startRecording` | `chrome.runtime.sendMessage({ type: 'START_RECORDING' })` | WIRED | recorder.ts dispatches in `onMessage` switch at `:649-654` |
---
## Data-Flow Trace (Level 4 — Real Data Production)
| Artifact | Data Variable | Source | Produces Real Data | Status |
| ------------------------------ | ------------------------------ | ----------------------------------------------------------------------- | ---------------------------- | -------- |
| `recorder.ts: segments` | `let segments: Blob[]` | `onSegmentStopped → new Blob(currentChunks) → push` driven by MediaRecorder.onstop | YES — real ~10 s WebM blobs from MediaRecorder | FLOWING |
| `recorder.ts: encodeAndSendBuffer` | `transferred[]` | `await blobToBase64(segment)` per segment, sent over port | YES — round-trip pinned by tests; empirical fixture proves end-to-end | FLOWING |
| `background/index.ts: mergeVideoSegments` | `finalBlob` | `new Blob(sortedSegments.map(s => s.data))` | YES — empirical fixture: 1.6 MB VP9 playable file | FLOWING |
| `tests/fixtures/last_30sec.webm` | n/a (empirical fixture) | regenerated 2026-05-15 (commit cd61cbc) against D-13 recorder | YES — ffprobe exit 0 + ffmpeg dry-run exit 0 + operator-confirmed Chrome playback | FLOWING |
---
## Behavioral Spot-Checks
| Behavior | Command | Result | Status |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| Vitest suite passes | `npx vitest run --reporter=dot` | Test Files 7 passed (7); Tests 40 passed (40); 3.06s | PASS |
| TypeScript type-check passes | `npx tsc --noEmit` | exit 0 (no output) | PASS |
| No `as any` or `@ts-ignore` in Phase 1 surface | `grep -RIn "as any\|@ts-ignore" src/offscreen/ src/background/index.ts src/shared/` | exit 1 (no matches) | PASS |
| No `tabCapture` / `chrome.alarms` residue | `grep -RIn "tabCapture\|chrome.alarms" src/` | exit 1 (no matches) | PASS |
| No legacy IndexedDB SW plumbing | `grep -rn "openIndexedDB\|VideoRecorderDB" src/ --include='*.ts'` | Only matches: orphan-cleanup `indexedDB.deleteDatabase('VideoRecorderDB')` at `background/index.ts:609-610` (intentional) | PASS |
| Legacy top-level `offscreen/` directory removed | `ls offscreen/` | exit 2 — directory does not exist | PASS |
| Vite inline-plugin removed | `grep -rn "copy-offscreen\|chromeMediaSource" vite.config.ts` | exit 1 (no matches) | PASS |
| Build produces loadable extension | `npm run build` | 60 modules transformed, exit 0; `dist/manifest.json`, `dist/assets/offscreen-*.js`, `dist/assets/index.ts-*.js` (SW) all produced | PASS |
| Fixture decodable by ffprobe (D-12 gate) | `ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm` | exit 0; empty stderr | PASS |
| Fixture passes A3 empirical decode dry-run | `ffmpeg -nostdin -v warning -i tests/fixtures/last_30sec.webm -f null -` | exit 0; **0** packet errors; **0** "File ended prematurely"; 298 DTS-monotonicity warnings (expected D-13 trade-off) + 1 "File extends beyond end of segment" (advisory, see below) | PASS |
---
## Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
| ------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| REQ-video-ring-buffer | Phase 1 (all 7 plans) | 30 s in-memory ring buffer via D-13 restart-segments; `getDisplayMedia` (AMENDED from `chrome.tabCapture`); vp9 @ 400 000 bps | SATISFIED | All structural + empirical + wiring evidence above; SPEC §10 #2/#3/#7 functionally green at Phase 1 level |
**Phase 4 (SPEC §10 smoke) still owns the canonical 9-criterion sweep.** Phase 1 only proved #2/#3/#7 against the codebase + fixture; #1 install, #6 < 5 s export latency, #8 password masking, #9 RAM ceiling, and end-to-end Chrome runtime confirmation remain Phase 4 scope. This is by design per REQUIREMENTS.md line 188.
---
## Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
| ----------------------------------- | -------- | ---------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| (none) | — | No TODO/FIXME/XXX/HACK in Phase 1 surface; no `as any` / `@ts-ignore`; no `console.log`-only handlers; no empty `=> {}` rendering paths; no hardcoded empty arrays/objects flowing to user-visible output | — | Clean. |
The `ffmpeg` stderr emits a single `File extends beyond end of segment` line — this is **Matroska-demuxer-level informational**, not a decoder error, and is the documented D-13 trade-off (multi-EBML-header concat). Recorded as ADVISORY below, not as an anti-pattern.
---
## Advisory Observations
These do NOT block Phase 1 completion but warrant attention in Phase 4 or Phase 5.
1. **ADV-1: ffmpeg emits one `[in#0/matroska,webm] File extends beyond end of segment` line.** This is the Matroska demuxer noting that the SECOND EBML segment in the concatenated WebM declares a smaller `SegmentSize` than the remaining bytes (because subsequent segments follow). Chrome's MSE pipeline handles this natively — operator-confirmed playback was clean on 2026-05-15. The `webm-playback.test.ts` acceptance gate does NOT assert against this signal (only `packetErrorCount === 0` and `endedPrematurely === false`), so it is correctly NOT a failure. Recommend documenting this signal in the Phase 5 cursor-visibility plan's "expected stderr from ffmpeg dry-run" allow-list to prevent future verifiers from being surprised by it.
2. **ADV-2: `blobToBase64` per-byte concat is O(n²) for large blobs.** WR-06 in REVIEW-FIX.md was pre-approved for Phase 2 deferral with the documented rationale (`String.fromCharCode(...subarray)` apply-spread argument-length limit ~64 KiB). For the current ~1.6 MB fixture this works fine; if recording quality rises (higher bitrate, longer windows, audio in CAP-01) the encode latency could grow non-linearly. Phase 2 quality-bump review should consider chunked-apply at 32 KiB stride per REVIEW.md's WR-06 recommendation.
---
## Operator-Side Residue (UAT scope)
These behaviors CANNOT be verified by this codebase verifier — they require a real Chrome runtime and an operator. Phase 4 SPEC §10 smoke pass MUST cover them:
1. **OPR-1: `getDisplayMedia` picker UX appears and grants stream on operator click.** Chrome's native screen-share picker dialog cannot be driven from Vitest's Node environment. Verification path: load `dist/` unpacked → click extension → observe picker dialog → pick "Entire screen" → confirm "Sharing your screen" indicator appears → SW console shows `[Offscreen:Recorder] Stream created`. (VALIDATION.md §"Manual-Only Verifications" row 1.)
2. **OPR-2: Continuous recording across tab switches under live `getDisplayMedia` stream.** Codebase verification confirms NO per-tab re-acquisition code exists (no `chrome.tabs.onActivated` handlers; recorder retains single `MediaStream` across rotations); this is a SUFFICIENT structural condition. Empirical confirmation that the recording does NOT pause when the operator switches tabs in Chrome requires a live session. Verification path: start recording, switch tabs 3-5 times over 30 s, export, confirm `last_30sec.webm` covers the full 30 s window. (VALIDATION.md §"Manual-Only Verifications" row 2 analogue, applied to amended D-15 semantics.)
3. **OPR-3: SW idle-unload survival.** D-16 + D-17 are structurally satisfied (offscreen owns buffer; long-lived port keeps SW alive; `hasDocument()` + CR-03 handshake fix on SW respawn). Real MV3 SW lifecycle behavior cannot be driven in Vitest. Verification path: DevTools → Extensions → Service Worker → "Force stop" → wait 60 s → click extension save → confirm exported archive contains video segments from before "Force stop". (VALIDATION.md §"Manual-Only Verifications" row 3.)
These three items are Phase 4's territory per ROADMAP.md (Phase 4 success criteria #2 and #4 in particular). This verifier does NOT classify them as Phase 1 gaps — Phase 1's `human_needed` status is the correct routing: ship Phase 1 as DELIVERED in code, hand the operator-side checks to Phase 4 smoke.
---
## Gaps Summary
No gaps. Phase 1's 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" — is achieved by the wired codebase:
- **30 s buffer:** `MAX_SEGMENTS=3 × SEGMENT_DURATION_MS=10_000 = 30_000` ms enforced in production (`onSegmentStopped:352-355`) AND pinned by tests (`segment-rotation.test.ts:84-104`, "caps at MAX_SEGMENTS — 4th push evicts the oldest", "many pushes never exceed the cap").
- **Continuous across tab switches:** Amended D-01/D-15 semantics under `getDisplayMedia` — no per-tab handlers exist, single MediaStream retained across MediaRecorder rotations (recorder.ts:46 comment + grep evidence).
- **Playable WebM:** D-13 restart-segments — each segment self-contained with own EBML header + seed keyframe (`startNewSegment:252-302` constructs fresh MediaRecorder per rotation); empirically verified by ffmpeg dry-run exit 0 + 0 packet errors + 0 ended-prematurely.
- **Assembled `last_30sec.webm` plays:** Empirical fixture `tests/fixtures/last_30sec.webm` (1.6 MB) passes both D-12 (ffprobe) and A3 (ffmpeg) gates, operator-confirmed Chrome playback on 2026-05-15.
The two architectural escalations encountered mid-phase (D-12 binary transfer + A3 keyframe orphan-P-frame) were both anticipated as HIGH-risk in RESEARCH.md and resolved by pre-staged fallbacks (base64 wire format + D-13 restart-segments); both fallbacks are now production code and pinned by tests.
---
_Verified: 2026-05-16T09:11:33Z_
_Verifier: Claude (gsd-verifier)_