--- phase: 01-stabilize-video-pipeline plan: 03 type: tdd wave: 1 depends_on: ["02"] files_modified: - src/offscreen/recorder.ts - src/offscreen/index.html - src/shared/logger.ts - src/shared/types.ts autonomous: true requirements: - REQ-video-ring-buffer requirements_addressed: - REQ-video-ring-buffer must_haves: truths: - "`src/offscreen/recorder.ts` exists and exports the symbols Plan 02 tests against: addChunk, trimAged, getBuffer, resetBuffer, assertCodecSupported, VIDEO_BUFFER_DURATION_MS" - "Running `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` exits 0 with all tests green" - "Buffer holds at most: 1 header chunk + every chunk with arrival timestamp newer than now-30_000ms" - "Codec strictly bound to `video/webm;codecs=vp9` at 400 000 bps with `MediaRecorder.isTypeSupported` gate; no fallback chain (D-20)" - "`MediaRecorder.start(2000)` is called on session start (timeslice = 2000 ms per SPEC §4.1)" - "On `MediaStreamTrack.onended`, the buffer is cleared and a `RECORDING_ERROR` of `'user-stopped-sharing'` is emitted to SW" - "Restart-segments fallback (D-13) is pre-staged as a commented-out skeleton at the bottom of recorder.ts so Plan 07's fallback path doesn't require a re-plan" - "`src/offscreen/index.html` exists at the source path and references `./recorder.ts`" - "`src/shared/logger.ts` has an `OffscreenLogger` class with `[OS:...]` prefix" - "`src/shared/types.ts` carries the new `PortMessageType` and `PortMessage` types; `VIDEO_CHUNK` and `VIDEO_CHUNK_SAVED` removed" artifacts: - path: "src/offscreen/recorder.ts" provides: "Ring buffer + getDisplayMedia + MediaRecorder + codec strict-mode + track-ended handler" min_lines: 150 exports: ["addChunk", "trimAged", "getBuffer", "resetBuffer", "assertCodecSupported", "VIDEO_BUFFER_DURATION_MS"] - path: "src/offscreen/index.html" provides: "crxjs-managed offscreen entry point" contains: "./recorder.ts" - path: "src/shared/logger.ts" provides: "OffscreenLogger added alongside existing Logger / ContentLogger" contains: "export class OffscreenLogger" - path: "src/shared/types.ts" provides: "Port message types added; deleted VIDEO_CHUNK / VIDEO_CHUNK_SAVED variants" contains: "PortMessage" key_links: - from: "src/offscreen/recorder.ts" to: "src/shared/types.ts" via: "import type { Message, VideoChunk, PortMessage } from '../shared/types'" pattern: "from '../shared/types'" - from: "src/offscreen/recorder.ts" to: "src/shared/logger.ts" via: "import { OffscreenLogger } from '../shared/logger'" pattern: "from '../shared/logger'" - from: "src/offscreen/index.html" to: "src/offscreen/recorder.ts" via: " ``` Notes: - The exported pure functions (`addChunk`, `trimAged`, `getBuffer`, `resetBuffer`, `assertCodecSupported`) are deliberately decoupled from the impure `getDisplayMedia` path so they can be exercised in Node — exactly what Plan 02's ring-buffer + codec tests do. - The handshake `OFFSCREEN_READY` send + the `chrome.runtime.connect` call are minimal stubs that satisfy the import-time test side-effects. Plan 04 elaborates them (adds ping loop, adds onDisconnect reconnect, adds REQUEST_BUFFER handler) — the changes Plan 04 makes are ADDITIVE within the same module, so they do NOT break Plan 03's tests. - The `as any` count in this module is zero, the `@ts-ignore` count is zero (per CLAUDE.md naming rule + CONTEXT.md "Established patterns"). After writing both files, run: ```bash npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts --reporter=dot npx tsc --noEmit ``` Both MUST exit 0. npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts && npx tsc --noEmit - `test -f src/offscreen/recorder.ts && test -f src/offscreen/index.html` exits 0 - `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` exits 0 with 6 passing tests - `npx tsc --noEmit` exits 0 - `grep -c "export function addChunk" src/offscreen/recorder.ts` returns 1 - `grep -c "export function trimAged" src/offscreen/recorder.ts` returns 1 - `grep -c "export function getBuffer" src/offscreen/recorder.ts` returns 1 - `grep -c "export function resetBuffer" src/offscreen/recorder.ts` returns 1 - `grep -c "export function assertCodecSupported" src/offscreen/recorder.ts` returns 1 - `grep -c "VIDEO_BUFFER_DURATION_MS = 30_000" src/offscreen/recorder.ts` returns 1 - `grep -v '^#' src/offscreen/recorder.ts | grep -cE "codecs=(vp8|h264)"` returns 0 (T-1-01 mitigation — no fallback codec) - `grep -c "as any" src/offscreen/recorder.ts` returns 0 (CLAUDE.md rule) - `grep -c "@ts-ignore" src/offscreen/recorder.ts` returns 0 - `grep -c "start(2000)" src/offscreen/recorder.ts` returns 0 BUT `grep -c "TIMESLICE_MS" src/offscreen/recorder.ts` returns at least 2 (start uses the constant) - `grep -c "./recorder.ts" src/offscreen/index.html` returns 1 Two tests files green; ring buffer and codec strict-mode behavior implemented; offscreen HTML entry exists; tsc clean. Task 3: Logger + types ergonomics — add OffscreenLogger, clean up shared/types.ts src/shared/logger.ts, src/shared/types.ts - src/shared/logger.ts (lines 1-51 — Logger and ContentLogger shapes) - src/shared/types.ts (lines 1-68 — full current state) - .planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md §"Shared Patterns / Logging" (lines 716-759) - .planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md §`src/shared/types.ts` (lines 480-530) Two edits. (1) `src/shared/logger.ts` — append a new `OffscreenLogger` class at the END of the file, mirroring the shape of `ContentLogger`: ```typescript // Логгер для Offscreen Document export class OffscreenLogger { private context: string; constructor(context: string) { this.context = context; } private logWithLevel(level: 'log' | 'warn' | 'error', ...args: any[]) { const timestamp = new Date().toISOString(); console[level](`[OS:${this.context}] ${timestamp}`, ...args); } log(...args: any[]) { this.logWithLevel('log', ...args); } warn(...args: any[]) { this.logWithLevel('warn', ...args); } error(...args: any[]) { this.logWithLevel('error', ...args); } } ``` Do not modify the existing `Logger` or `ContentLogger` classes. (2) `src/shared/types.ts` — three edits: (a) Remove `'VIDEO_CHUNK'` and `'VIDEO_CHUNK_SAVED'` from the `MessageType` union. The `MessageType` becomes: ```typescript export type MessageType = | 'REQUEST_PERMISSIONS' | 'PERMISSIONS_GRANTED' | 'PERMISSIONS_DENIED' | 'GET_VIDEO_BUFFER' | 'VIDEO_BUFFER_RESPONSE' | 'GET_RRWEB_EVENTS' | 'RRWEB_EVENTS_RESPONSE' | 'SAVE_ARCHIVE' | 'ARCHIVE_SAVED' | 'START_RECORDING' | 'STOP_RECORDING' | 'RECORDING_ERROR' | 'OFFSCREEN_READY'; ``` (b) After the existing `Message` interface (after line 24), add the port message types VERBATIM: ```typescript // Типы сообщений в long-lived port (offscreen ↔ SW; D-17 / Plan 04) export type PortMessageType = | 'PING' | 'REQUEST_BUFFER' | 'BUFFER'; export interface PortMessage { type: PortMessageType; chunks?: VideoChunk[]; } ``` (c) Leave the `VideoChunk`, `UserEvent`, `SessionMetadata`, `PopupState`, `VideoBufferResponse`, `RrwebEventsResponse` interfaces unchanged. After both edits, run: ```bash npx tsc --noEmit npx vitest run ``` Both MUST exit 0 (the 2 ring-buffer + codec tests should still pass; the 2 handshake + port tests remain RED — they get GREEN in Plan 04). npx tsc --noEmit && grep -c "OffscreenLogger" src/shared/logger.ts && grep -cE "VIDEO_CHUNK[^_S]|VIDEO_CHUNK_SAVED" src/shared/types.ts - `npx tsc --noEmit` exits 0 - `grep -c "export class OffscreenLogger" src/shared/logger.ts` returns 1 - `grep -c "'VIDEO_CHUNK'" src/shared/types.ts` returns 0 (removed) - `grep -c "'VIDEO_CHUNK_SAVED'" src/shared/types.ts` returns 0 (removed) - `grep -c "PortMessage" src/shared/types.ts` returns at least 2 (one for the type alias, one for the interface) - `grep -c "OFFSCREEN_READY" src/shared/types.ts` returns 1 (still present) - `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` exits 0 OffscreenLogger available; types cleaned up; ring-buffer + codec tests still green; the deleted message types prevent Plan 05 from accidentally re-introducing the broken sendMessage-Blob path. Task 4: REFACTOR — optional cleanup pass (only if obvious improvements exist) src/offscreen/recorder.ts (only if changed) - src/offscreen/recorder.ts (Task 2 output) Per the TDD REFACTOR phase rules in `$HOME/.claude/get-shit-done/references/tdd.md`: 1. Re-read `src/offscreen/recorder.ts` (from Task 2). 2. Look ONLY for obvious improvements: - Constant duplication - Unused imports - Mis-placed comments 3. If no obvious improvements: SKIP THIS TASK (do not commit). Note in the summary: "Refactor: no changes needed." 4. If improvements found: make them; run `npx vitest run && npx tsc --noEmit`; commit ONLY if both exit 0. Do NOT use this task to add features. Do NOT rewrite the bootstrap section (that's Plan 04's territory). Do NOT extract the ring buffer into a separate file in this phase (audit-driven decision: one source of truth in `src/offscreen/recorder.ts`). npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts && npx tsc --noEmit - All Plan-03-owned tests still pass after the refactor pass (or after the no-op decision) - `npx tsc --noEmit` still exits 0 - The SUMMARY.md documents whether refactor happened or was skipped REFACTOR phase complete (or explicitly skipped with rationale). After all four tasks land: 1. `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` — exits 0, 6 tests passing. 2. `npx vitest run tests/offscreen/handshake.test.ts tests/offscreen/port.test.ts` — exits NON-ZERO (these stay RED until Plan 04). The failure mode is now NOT a module-resolution error (since the module exists), but rather an assertion mismatch — the stub bootstrap calls `chrome.runtime.sendMessage({type: 'OFFSCREEN_READY'})` exactly once and `chrome.runtime.connect({name: 'video-keepalive'})` exactly once, so the handshake test MAY actually pass after Plan 03 lands. **This is acceptable**: the reconnect test is what definitively pins Plan 04. 3. `npx tsc --noEmit` — exits 0. 4. `grep -c "as any\|@ts-ignore" src/offscreen/recorder.ts src/offscreen/index.html` — returns 0. 5. `grep -v '^#' src/offscreen/recorder.ts | grep -cE "codecs=(vp8|h264)"` — returns 0 (T-1-01 mitigation grep gate). 6. `wc -l src/offscreen/recorder.ts` — at least 150 lines. Commit cadence: per the TDD reference, this is the GREEN phase of a TDD plan: - Task 1: no commit (verify-only). - Task 2: ONE commit (`feat(01-03): implement offscreen recorder ring buffer and codec strict-mode`). - Task 3: ONE commit (`feat(01-03): add OffscreenLogger and clean up shared types`). - Task 4: zero or one commit (`refactor(01-03): {what was cleaned}` only if changes were made). Total: 2-3 commits. - `src/offscreen/recorder.ts` exists, exports all 6 symbols Plan 02 tests expect - 6 ring-buffer + codec tests passing (Plan 02 → Plan 03 GREEN handoff complete for the ring-buffer + codec slice) - `src/offscreen/index.html` exists at source path, references `./recorder.ts` - `OffscreenLogger` class exists in `src/shared/logger.ts` - `PortMessage` type exists in `src/shared/types.ts`; `VIDEO_CHUNK` / `VIDEO_CHUNK_SAVED` removed - D-13 fallback skeleton present as comment block in recorder.ts (no re-plan needed if Plan 07 ffprobe fails) - `npx tsc --noEmit` clean; no `as any`, no `@ts-ignore` - The grep gates in `` all return their expected values After completion, create `.planning/phases/01-stabilize-video-pipeline/01-03-SUMMARY.md` with: - RED: log excerpt from Task 1 showing the module-resolution failure - GREEN: list of 6 tests now passing, with the test names - REFACTOR: what changed (if anything) in Task 4 - Final list of files modified (4 files) and line counts - The exact export surface of recorder.ts (so Plan 04 / 05 can grep against it without re-reading) - Note: "Plan 04 needs to: (a) replace the import-time stub `chrome.runtime.connect({name:'video-keepalive'})` with the full ping-loop + reconnect-on-disconnect from RESEARCH.md Pattern 5; (b) wire the `REQUEST_BUFFER` handler so the SW can pull chunks on export; (c) confirm the existing `OFFSCREEN_READY` send is still emitted exactly once. Plan 05 needs to update SW side accordingly." - Commit list (2-3 commits)