docs(01): add Plans 01-08 / 01-09 / 01-10 (amended Phase 1 charter)
Plans cover the post-D-13 architecture and the auto-start UX charter expansion that landed during 2026-05-16 UAT: - Plan 01-08 — WebM remux via ts-ebml@3.0.2 + webm-muxer@5.1.4. Replaces the broken file-concat in mergeVideoSegments with a real single-EBML remux. Drives the 2 RED tests in tests/offscreen/webm-playback.test.ts to GREEN. Regenerates the canonical fixture against the remuxer. 5 tasks (4 TDD + 1 operator empirical checkpoint), wave 1. - Plan 01-09 — Whole-desktop constraint (displaySurface:'monitor', cursor:'always') + post-grant validation, chrome.action.onClicked direct toolbar invocation, chrome.action badge state machine (REC/OFF/ERROR), chrome.runtime.onStartup notification + recovery notification on onUserStoppedSharing, popup scoped to SAVE-only. 17 new test assertions across 4 test files. smoke.sh updated to auto-select an entire screen. 5 tasks (4 TDD + 1 operator empirical checkpoint), wave 2 (depends on 01-08). - Plan 01-10 — chrome.runtime.onInstalled welcome tab on first install via chrome.storage.local guard; vanilla welcome.html/ts/css bundle with single "Начать запись" button consuming install-time activation. Uses centralized Logger pattern. 4 tasks (3 TDD + 1 operator empirical checkpoint), wave 3 (depends on 01-09). CONTEXT.md amendment block appended with 4 disambiguated decisions: - D-14-remux: WebM remux supersedes D-13 file-concat - D-15-display-surface: whole-desktop + cursor visibility lifted from Phase 5 deferral - D-16-toolbar: toolbar onClicked + popup SAVE-only + badge state machine + onStartup/recovery notifications - D-17-onboarding: welcome tab on first install (distinct from D-17-port-lifecycle from Option C) The earlier D-17 port-lifecycle heading also renamed to hyphenated form for cross-ref consistency. Plan-check loop: 3 iterations (initial + 2 revisions). Iteration 1 surfaced 11 findings (2 BLOCKER + 6 WARNING + 3 INFO); all addressed via revision iter 1 with checker-recommended fixes. Iteration 2 surfaced 3 derivative regressions (literal-string grep anchors from the iter-1 fixes did not match live CONTEXT.md); all addressed in iter 2 with empirically-validated literals. Iteration 3 PASSED clean. Validation: gsd-sdk frontmatter.validate + verify.plan-structure both return valid=True for all 3 plans. Plan 01-08 Task 4 verify-chain grep tested end-to-end against live CONTEXT.md (exit 0). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
497
.planning/phases/01-stabilize-video-pipeline/01-08-PLAN.md
Normal file
497
.planning/phases/01-stabilize-video-pipeline/01-08-PLAN.md
Normal file
@@ -0,0 +1,497 @@
|
||||
---
|
||||
phase: 01-stabilize-video-pipeline
|
||||
plan: 08
|
||||
type: tdd
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- package.json
|
||||
- package-lock.json
|
||||
- src/background/index.ts
|
||||
- src/background/webm-remux.ts
|
||||
- tests/background/webm-remux.test.ts
|
||||
- tests/fixtures/last_30sec.webm
|
||||
- .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
|
||||
autonomous: false
|
||||
requirements:
|
||||
- REQ-video-ring-buffer
|
||||
tags:
|
||||
- webm
|
||||
- remux
|
||||
- ts-ebml
|
||||
- webm-muxer
|
||||
- vp9
|
||||
- matroska
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "tests/offscreen/webm-playback.test.ts container-level format=duration assertion (>= 25_000 ms) is GREEN against the regenerated tests/fixtures/last_30sec.webm fixture."
|
||||
- "tests/offscreen/webm-playback.test.ts ffmpeg full-decode timeline assertion (>= 25_000 ms) is GREEN against the regenerated fixture."
|
||||
- "mergeVideoSegments() no longer exists in src/background/index.ts; its callers go through the new remuxSegments() in src/background/webm-remux.ts."
|
||||
- "The remux produces a single-EBML-header WebM (verified by byte-level probe asserting EBML header occurrence count === 1) covering all VP9 frames from every input segment with monotonically increasing timestamps."
|
||||
- "The 53 baseline GREEN tests across 11 files plus the 4 webm-playback tests all pass (`npx vitest run` exit 0)."
|
||||
- "Operator running ./smoke.sh + opening the produced last_30sec.webm in Chrome AND mpv sees ~30 s of continuous playback (NOT ~9 s)."
|
||||
artifacts:
|
||||
- path: "src/background/webm-remux.ts"
|
||||
provides: "remuxSegments(segments: VideoSegment[]): Promise<Blob> — parses each input segment via ts-ebml, extracts VP9 SimpleBlocks + cluster timecodes, re-mixes monotonic timestamps via webm-muxer into a single-EBML-headered WebM."
|
||||
min_lines: 120
|
||||
exports:
|
||||
- "remuxSegments"
|
||||
- path: "tests/background/webm-remux.test.ts"
|
||||
provides: "Unit-level RED→GREEN tests for the remux helper: single-EBML invariant, monotonic-timestamp invariant, frame-count preservation across input segments, keyframe-flag preservation."
|
||||
min_lines: 100
|
||||
- path: "src/background/index.ts"
|
||||
provides: "createArchive() calls remuxSegments() (await — async) instead of synchronous mergeVideoSegments(); EmptyVideoBufferError surface preserved."
|
||||
contains: "remuxSegments"
|
||||
- path: "tests/fixtures/last_30sec.webm"
|
||||
provides: "Regenerated fixture captured against the post-remux build; ffprobe container duration >= 25_000 ms; ffmpeg full-decode time >= 25_000 ms."
|
||||
- path: "package.json"
|
||||
provides: "Adds `ts-ebml` ^3.0.2 and `webm-muxer` ^5.1.4 to dependencies (both MIT, both SW-compatible per the d13 debug session library survey)."
|
||||
contains: "ts-ebml"
|
||||
- path: ".planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md"
|
||||
provides: "AMENDMENT block appending D-14-remux (ts-ebml + webm-muxer remux) supersedes D-13's file-concat merge; D-13's recorder-side restart-segments lifecycle preserved."
|
||||
contains: "D-14-remux"
|
||||
key_links:
|
||||
- from: "src/background/index.ts:createArchive"
|
||||
to: "src/background/webm-remux.ts:remuxSegments"
|
||||
via: "await remuxSegments(videoBufferResponse.segments)"
|
||||
pattern: "remuxSegments\\("
|
||||
- from: "src/background/webm-remux.ts"
|
||||
to: "ts-ebml package"
|
||||
via: "import { Decoder } from 'ts-ebml'"
|
||||
pattern: "from ['\"]ts-ebml['\"]"
|
||||
- from: "src/background/webm-remux.ts"
|
||||
to: "webm-muxer package"
|
||||
via: "import { Muxer, ArrayBufferTarget } from 'webm-muxer'"
|
||||
pattern: "from ['\"]webm-muxer['\"]"
|
||||
- from: "tests/offscreen/webm-playback.test.ts"
|
||||
to: "tests/fixtures/last_30sec.webm"
|
||||
via: "ffprobe + ffmpeg drive the regenerated fixture; the 2 RED assertions (lines 232-316) flip GREEN."
|
||||
pattern: "MIN_PLAYABLE_DURATION_MS"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Replace `src/background/index.ts:mergeVideoSegments()` file-concat (D-13) with a real single-EBML-headered WebM **remux** via `ts-ebml` (parse) + `webm-muxer` (write). Each input segment carries its own EBML header + Segment + Cluster structure; the output is one EBML + one Segment whose clusters contain every VP9 SimpleBlock from every input segment, with timestamps adjusted to be monotonic against a single global timeline. D-13's recorder-side restart-segments lifecycle (the part that fixed orphan-P-frame freezes) is preserved; only the merge step is replaced.
|
||||
|
||||
Purpose: satisfy SPEC §10 #7 (`last_30sec.webm` plays back in a browser) for real. The 2026-05-15 closure shipped a multi-EBML concat that mpv / Chrome / ffprobe all play as ~9.94 s (first segment's Info.Duration only) instead of ~30 s. The two RED tests already on disk in `tests/offscreen/webm-playback.test.ts` (lines 231-316) pin the actual contract; this plan drives them GREEN.
|
||||
|
||||
Output:
|
||||
- `src/background/webm-remux.ts` — new module exporting `remuxSegments(segments: VideoSegment[]): Promise<Blob>`.
|
||||
- `src/background/index.ts` — `mergeVideoSegments()` deleted; `createArchive()` calls `await remuxSegments(...)` (becomes async-on-the-merge-path; saveArchive already awaits createArchive).
|
||||
- `tests/background/webm-remux.test.ts` — RED→GREEN unit tests pinning the single-EBML / monotonic-timestamp / frame-count / keyframe-flag invariants without requiring a real MediaRecorder.
|
||||
- Regenerated `tests/fixtures/last_30sec.webm` from a fresh `./smoke.sh` against the post-remux build, validated by the 2 new playback-duration assertions.
|
||||
- `package.json` + `package-lock.json` — adds `ts-ebml` ^3.0.2 + `webm-muxer` ^5.1.4.
|
||||
- CONTEXT.md amendment block (D-14-remux supersedes D-13's file-concat; D-13's recorder lifecycle preserved). The amendment block was authored at plan-creation time by the orchestrator; Task 4 step 9 verifies it remains intact via grep checks (no new file mutation expected).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
|
||||
@.planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md
|
||||
@.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md
|
||||
@.planning/phases/01-stabilize-video-pipeline/01-UAT.md
|
||||
@.planning/debug/d13-multi-ebml-concat-unplayable.md
|
||||
@tests/offscreen/webm-playback.test.ts
|
||||
@src/background/index.ts
|
||||
@src/shared/types.ts
|
||||
@src/shared/binary.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor will use. Extracted from codebase. -->
|
||||
<!-- Executor should use these directly — no codebase exploration needed for them. -->
|
||||
|
||||
From `src/shared/types.ts`:
|
||||
```typescript
|
||||
export interface VideoSegment {
|
||||
data: Blob; // a self-contained ~10 s WebM blob (D-13 segment)
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface VideoBufferResponse {
|
||||
segments: VideoSegment[];
|
||||
}
|
||||
```
|
||||
|
||||
From `src/background/index.ts` (current call site to be replaced, lines 395-421 + 449):
|
||||
```typescript
|
||||
// CURRENT (D-13 file-concat — TO BE DELETED):
|
||||
function mergeVideoSegments(segments: VideoSegment[]): Blob {
|
||||
const sortedSegments = [...segments].sort((a, b) => a.timestamp - b.timestamp);
|
||||
const blobs: Blob[] = sortedSegments.map((segment) => segment.data);
|
||||
return new Blob(blobs, { type: 'video/webm' });
|
||||
}
|
||||
|
||||
// Called at line 449 (inside createArchive):
|
||||
const videoBlob = mergeVideoSegments(videoBufferResponse.segments);
|
||||
```
|
||||
|
||||
From the debug session research (`.planning/debug/d13-multi-ebml-concat-unplayable.md` lines 380-410), the remux algorithm:
|
||||
```
|
||||
1. For each segment blob, parse via ts-ebml Decoder → walk EBML tree →
|
||||
for each Cluster's SimpleBlock children, extract:
|
||||
• VP9 frame bytes (the SimpleBlock payload after the per-block header)
|
||||
• keyframe flag (Matroska SimpleBlock first-flag-byte bit 7)
|
||||
• cluster Timestamp (Cluster.Timecode element, ms)
|
||||
• block-local timestamp offset (relative to its enclosing cluster)
|
||||
2. Compute monotonic adjusted timestamp:
|
||||
globalUs = (segmentBaseMs + clusterTsMs + blockOffsetMs) * 1000
|
||||
where segmentBaseMs accumulates the prior segment's total content
|
||||
duration (the last block's local timestamp + 1 frame worth, ~33 ms).
|
||||
3. Stream all adjusted frames into a single webm-muxer Muxer with
|
||||
addVideoChunkRaw(frameData, isKey ? 'key' : 'delta', globalUs).
|
||||
4. muxer.finalize() → target.buffer (ArrayBuffer) → wrap in Blob.
|
||||
```
|
||||
|
||||
From `webm-muxer` 5.1.4 API surface (from npm registry; see https://github.com/Vanilagy/webm-muxer):
|
||||
```typescript
|
||||
import { Muxer, ArrayBufferTarget } from 'webm-muxer';
|
||||
|
||||
const target = new ArrayBufferTarget();
|
||||
const muxer = new Muxer({
|
||||
target,
|
||||
video: {
|
||||
codec: 'V_VP9', // Matroska codec id for VP9
|
||||
width: <px>, height: <px>, // probe from first segment's Video element
|
||||
frameRate: 30, // approximate — not strict
|
||||
},
|
||||
// No `audio` block — Phase 1 SPEC §9 / CAP-01 explicitly excludes audio.
|
||||
type: 'webm', // emit a WebM (Matroska subset) container
|
||||
firstTimestampBehavior: 'offset', // tolerate a non-zero first frame ts
|
||||
});
|
||||
|
||||
// timestamp is MICROSECONDS, integer.
|
||||
muxer.addVideoChunkRaw(data: Uint8Array, type: 'key' | 'delta', timestamp: number, meta?: { decoderConfig?: { description?: Uint8Array } });
|
||||
|
||||
muxer.finalize();
|
||||
// target.buffer is now an ArrayBuffer holding the single-EBML WebM.
|
||||
```
|
||||
|
||||
From `ts-ebml` 3.0.2 API surface (from npm registry; see https://github.com/legokichi/ts-ebml):
|
||||
```typescript
|
||||
import { Decoder, tools } from 'ts-ebml';
|
||||
|
||||
const decoder = new Decoder();
|
||||
const elements = decoder.decode(arrayBuffer); // EBMLElementDetail[]
|
||||
// Walk elements; each has { name, type, dataSize, value, isEnd, ... }.
|
||||
// Element names of interest (Matroska element table):
|
||||
// 'EBML', 'Segment', 'Tracks', 'TrackEntry', 'Video', 'PixelWidth',
|
||||
// 'PixelHeight', 'CodecPrivate', 'Cluster', 'Timecode', 'SimpleBlock'.
|
||||
// SimpleBlock payload is the raw block: variable-length track number prefix,
|
||||
// 2 bytes signed timestamp delta, 1 flags byte (bit 7 = keyframe), then
|
||||
// VP9 frame bytes.
|
||||
```
|
||||
|
||||
Existing test infrastructure (reusable patterns from `tests/background/request-id-protocol.test.ts` and `tests/offscreen/codec-check.test.ts`):
|
||||
- `vi.resetModules()` + dynamic `await import('../../src/background/...')` per test (fresh module state).
|
||||
- No chrome stub needed for `webm-remux.ts` — it's a pure async function.
|
||||
- `import.meta.url` + `node:fs` for fixture reads.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Install ts-ebml + webm-muxer; verify SW-compat by import-only test (Wave 0 contract).</name>
|
||||
<read_first>
|
||||
- package.json (current dependency list — confirm neither library is already installed)
|
||||
- vite.config.ts (confirm crxjs picks up new imports automatically; no Rollup external config needed)
|
||||
- .planning/debug/d13-multi-ebml-concat-unplayable.md, Evidence/library-survey section (lines 380-410) — confirms both libraries are SW-compatible (no `window`/`document` references)
|
||||
- tests/offscreen/codec-check.test.ts (test scaffold pattern: vi.resetModules + dynamic import)
|
||||
</read_first>
|
||||
<files>package.json, package-lock.json, tests/background/webm-remux-deps.test.ts</files>
|
||||
<behavior>
|
||||
- Test 1: `tests/background/webm-remux-deps.test.ts` imports `webm-muxer` and `ts-ebml` and asserts both exports surface (`Muxer`, `ArrayBufferTarget` from webm-muxer; `Decoder` from ts-ebml). RED if either is missing from `node_modules`; GREEN after `npm install` lands.
|
||||
- Test 2: same file asserts that loading both modules under Node's default globals (no jsdom shim) does not throw any `window is not defined`/`document is not defined` errors. RED if either dist accidentally hits a DOM global; GREEN by virtue of both libraries' published SW-compatibility (per debug session lines 380-410).
|
||||
</behavior>
|
||||
<action>
|
||||
1. RED:
|
||||
- Create `tests/background/webm-remux-deps.test.ts` with two `it` blocks:
|
||||
(a) `it('exports Muxer + ArrayBufferTarget + Decoder', async () => {...})` — dynamic-import both, expect named exports defined.
|
||||
(b) `it('loads under default Node globals without DOM-global ReferenceErrors', async () => {...})` — assert that `delete (globalThis as any).window; delete (globalThis as any).document;` followed by re-import does not throw.
|
||||
- Run `npx vitest run tests/background/webm-remux-deps.test.ts`; expect both tests RED with `Cannot find module` errors (libraries not installed yet).
|
||||
2. GREEN:
|
||||
- `npm install --save ts-ebml@^3.0.2 webm-muxer@^5.1.4` (versions verified live on npm registry 2026-05-16: ts-ebml 3.0.2 + webm-muxer 5.1.4; both MIT; both active).
|
||||
- Commit `package.json` + `package-lock.json` changes; do NOT touch other files.
|
||||
- Re-run the test; both flip GREEN.
|
||||
3. Re-run the full suite (`npx vitest run`) to confirm no collateral regression (53 baseline GREEN + 2 new GREEN = 55; the 2 existing RED tests in webm-playback.test.ts stay RED — they're Task 4's GREEN target).
|
||||
Use absolute imports (`import { Muxer } from 'webm-muxer'`, `import { Decoder } from 'ts-ebml'`) per the user's global style guide. No `as any`; no `@ts-ignore`. If TypeScript complains about missing types, install `@types/...` packages if available OR add a minimal `.d.ts` shim under `src/shared/` (whichever is canonical for the library's published shape — check the library's repo first, do NOT hand-roll types if the library ships its own).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run tests/background/webm-remux-deps.test.ts && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `package.json` dependencies include `ts-ebml` and `webm-muxer` at the pinned versions.
|
||||
- `tests/background/webm-remux-deps.test.ts` exists with the 2 tests above and both PASS.
|
||||
- `npx tsc --noEmit` exits 0 (no type errors from either library import).
|
||||
- `npx vitest run` shows 11 files / 53 GREEN + 2 new GREEN; the 2 RED in webm-playback.test.ts remain RED (this task does not touch the production code path yet).
|
||||
</acceptance_criteria>
|
||||
<done>Both libraries installed with pinned versions, the import-surface test pinning both library names is committed and GREEN, tsc clean, baseline suite preserved.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Add unit-level RED tests for remuxSegments() invariants (single-EBML, monotonic ts, frame-count, keyframe-flag).</name>
|
||||
<read_first>
|
||||
- tests/offscreen/segment-rotation.test.ts (existing test pattern for D-13; reuse the `Blob`-fixture-building style — uses node `Buffer`)
|
||||
- tests/offscreen/webm-playback.test.ts lines 1-100 (FIXTURE_PATH idiom + ffprobe/ffmpeg helpers; we'll reuse the ffprobe binary path for any container-introspection assertions in this test file)
|
||||
- .planning/debug/d13-multi-ebml-concat-unplayable.md, Evidence/H4 byte-level EBML probe section (lines 311-373) — describes the exact EBML element IDs to scan for: EBML header `0x1A 0x45 0xDF 0xA3`, Segment `0x18 0x53 0x80 0x67`, Cluster `0x1F 0x43 0xB6 0x75`.
|
||||
- src/shared/types.ts (VideoSegment shape).
|
||||
- tests/background/request-id-protocol.test.ts lines 53-85 (vi.fn / port stub pattern — but webm-remux.ts needs no chrome stub).
|
||||
</read_first>
|
||||
<files>tests/background/webm-remux.test.ts</files>
|
||||
<behavior>
|
||||
- Test 1 (RED → GREEN by Task 3): given the canonical 3-segment fixture `tests/fixtures/last_30sec.webm` sliced into 3 `Blob` segments (via the byte-offset probe in the debug session: seg1 = bytes [0, 509038), seg2 = bytes [509038, 970967), seg3 = bytes [970967, EOF)), `remuxSegments([seg1, seg2, seg3])` returns a `Blob` whose bytes contain EXACTLY 1 occurrence of the EBML header magic `[0x1A, 0x45, 0xDF, 0xA3]` and EXACTLY 1 occurrence of the Segment magic `[0x18, 0x53, 0x80, 0x67]`.
|
||||
- Test 2: the same output blob's byte size is within [0.7×, 1.3×] of the sum of input segment sizes — sanity check that we're not silently dropping content NOR ballooning it 10x.
|
||||
- Test 3: `ffprobe -v error -show_entries format=duration -of csv=p=0` against the output blob (write to tmpfile first) reports a duration >= 25_000 ms (skip-if-missing per webm-playback.test.ts pattern — use `existsSync(FFPROBE_BIN)` gate).
|
||||
- Test 4: `ffprobe -count_frames -show_entries stream=nb_read_frames` against the output blob reports a frame count `n` where `905 <= n <= 912` (the per-segment counts from the debug session probe were `301+300+311 = 912`; the muxer may drop at most one partial frame per segment boundary, so 3 boundaries × max 1 partial frame each = ±3 frames absolute tolerance). I-01 fix (2026-05-16 checker pass): tightened from the prior ±20% band (which would accept catastrophic loss like 730 frames) to ±1% / ±3 absolute frames, sized to the documented muxer boundary partial-frame drop only.
|
||||
- Test 5: empty-input → `remuxSegments([])` returns a `Promise<Blob>` whose blob has `.size === 0`. (Defense-in-depth; saveArchive's EmptyVideoBufferError throw guards this path upstream, but the helper should be safe on its own.)
|
||||
</behavior>
|
||||
<action>
|
||||
1. Create `tests/background/webm-remux.test.ts` with the 5 `it` blocks described above.
|
||||
2. Reuse the `FFPROBE_BIN` constant + `ffprobeAvailable()` skip-gate from `tests/offscreen/webm-playback.test.ts` (copy them inline — the test file lives in a different directory so a direct import is awkward; keep both copies trivially in sync per the existing webm-playback.test.ts pattern).
|
||||
3. Use `node:fs` `readFileSync` + `Buffer.subarray(start, end)` to slice the fixture into 3 segment Blobs (use `new Blob([buffer])` to wrap each slice). Add a `splitFixtureIntoSegments(): Blob[]` helper at the top of the test file documenting the byte offsets from the debug session probe.
|
||||
4. To find EBML element occurrences, write a tiny `countMagic(bytes: Uint8Array, magic: Uint8Array): number` helper that does a simple byte-window scan (no library needed — 4-byte patterns, no false-positive risk at fixture scale). The Cluster magic should appear >= 3 times in the output (one Cluster per ~1-3 s); but ASSERT only that EBML headers and Segments are EXACTLY 1 each.
|
||||
5. Use `await import('../../src/background/webm-remux')` (dynamic import) so the import error in this task (the file doesn't exist yet) is the test failure signal rather than a Vitest collection error. Wrap each `it` body in try/catch + `expect.fail()` if the import itself throws — gives a readable RED message rather than `Cannot find module`.
|
||||
6. Run `npx vitest run tests/background/webm-remux.test.ts`; expect ALL 5 tests RED (the module doesn't exist yet).
|
||||
7. DO NOT IMPLEMENT THE MODULE YET — Task 3 is the GREEN side of this RED gate.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run tests/background/webm-remux.test.ts 2>&1 | grep -E "Tests.*[0-9]+ failed"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `tests/background/webm-remux.test.ts` exists with 5 tests.
|
||||
- All 5 tests are RED (production module not yet implemented).
|
||||
- Baseline 55 tests (53 + 2 from Task 1) still GREEN; the 2 webm-playback duration RED still RED; 5 new RED here. Total: 11 files / 60 tests / 7 failed | 53 passed. tsc still exit 0 (the test file is type-clean — uses VideoSegment from shared types).
|
||||
- No file under `src/` is modified by this task.
|
||||
</acceptance_criteria>
|
||||
<done>5 RED tests committed pinning the remux invariants at the unit level; baseline preserved.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Implement remuxSegments() in src/background/webm-remux.ts — drives all 5 RED tests in webm-remux.test.ts to GREEN.</name>
|
||||
<read_first>
|
||||
- tests/background/webm-remux.test.ts (the contract — produced by Task 2)
|
||||
- .planning/debug/d13-multi-ebml-concat-unplayable.md, Evidence/library-survey + the algorithm description on lines 380-410 (the remux pipeline this implementation realizes)
|
||||
- src/shared/types.ts (VideoSegment + VideoBufferResponse)
|
||||
- src/shared/logger.ts (Logger class shape — same prefix-based instantiation used in SW; pick `Logger('Remux')` or equivalent)
|
||||
- The `ts-ebml` and `webm-muxer` README files in node_modules/ (installed by Task 1; READ the published library docs there rather than guessing API surface — the interfaces block in this plan is a sketch, the dist source is authoritative).
|
||||
</read_first>
|
||||
<files>src/background/webm-remux.ts</files>
|
||||
<behavior>
|
||||
Implements `export async function remuxSegments(segments: VideoSegment[]): Promise<Blob>` satisfying all 5 tests from Task 2. See behavior list there.
|
||||
</behavior>
|
||||
<action>
|
||||
1. Create `src/background/webm-remux.ts` with extensive JSDoc per project style (the user's global guide mandates extensive docstrings; mirror the existing src/background/index.ts comment density). Header comment cites D-14-remux (this plan's CONTEXT amendment, disambiguated from the historical "D-14: Not applicable" tab-switch entry per B-02 fix), references the d13 debug session, and explains the parse → adjust-timestamps → re-emit pipeline.
|
||||
2. Imports:
|
||||
- `import { Decoder } from 'ts-ebml';`
|
||||
- `import { Muxer, ArrayBufferTarget } from 'webm-muxer';`
|
||||
- `import type { VideoSegment } from '../shared/types';`
|
||||
- `import { Logger } from '../shared/logger';`
|
||||
Use absolute-style (no `from '..'` ascending more than one level except shared/) — matches existing src/background/index.ts.
|
||||
3. Helper functions (small + named — no `continue`, prefer if-else chains per project style):
|
||||
- `async function blobToArrayBuffer(b: Blob): Promise<ArrayBuffer>` — uses `b.arrayBuffer()` (web standard; available in Chrome SW).
|
||||
- `interface ExtractedFrame { data: Uint8Array; isKey: boolean; timestampMs: number; }`
|
||||
- `function extractFramesFromSegment(buffer: ArrayBuffer, segmentBaseMs: number): { frames: ExtractedFrame[]; segmentDurationMs: number; trackInfo: { width: number; height: number; codecPrivate?: Uint8Array; } | null }` — walks `Decoder.decode(buffer)`, tracks current Cluster Timecode, processes each SimpleBlock by parsing its first-byte-after-track-number flags (bit 7 = keyframe). Returns adjusted-by-segmentBaseMs timestamps.
|
||||
- SimpleBlock parsing: read VINT track number (Matroska VINT decode is library-trivial; copy the routine from the Matroska spec's "Variable Size Integer" section if ts-ebml does not surface it directly), then 2 bytes int16 BE timestamp delta, then 1 byte flags. Frame data = remaining bytes.
|
||||
- `function pickTrackInfoFromSegment(elements: EBMLElementDetail[]): { width, height, codecPrivate? } | null` — walks for Tracks → TrackEntry → Video → PixelWidth + PixelHeight; CodecPrivate if present.
|
||||
4. Main flow:
|
||||
a) Guard empty input: `if (segments.length === 0) return new Blob([], { type: 'video/webm' });` (satisfies Test 5).
|
||||
b) Sort by `timestamp` ascending (matches the SW-side ordering pattern in the deleted mergeVideoSegments).
|
||||
c) Parse first segment to derive `trackInfo` (width/height/CodecPrivate) — needed for the muxer's video config.
|
||||
d) Build a `Muxer<ArrayBufferTarget>` with:
|
||||
```
|
||||
codec: 'V_VP9', width, height, frameRate: 30,
|
||||
type: 'webm', firstTimestampBehavior: 'offset',
|
||||
```
|
||||
If `CodecPrivate` is present, pass it as `decoderConfig.description` (Uint8Array) via the per-chunk meta. (Most VP9 streams from MediaRecorder do not need CodecPrivate, but pass it through defensively.)
|
||||
e) Loop segments with an accumulating `segmentBaseMs`:
|
||||
- `extractFramesFromSegment(buffer, segmentBaseMs)` → frames + `segmentDurationMs`
|
||||
- For each frame: `muxer.addVideoChunkRaw(frame.data, frame.isKey ? 'key' : 'delta', frame.timestampMs * 1000)` (the muxer wants microseconds).
|
||||
- `segmentBaseMs += segmentDurationMs` (the next segment's first frame slots in just after the prior segment's last frame).
|
||||
f) `muxer.finalize()`, then `return new Blob([target.buffer], { type: 'video/webm' })`.
|
||||
5. Log at INFO: input segment count + sizes; per-segment extracted-frame count + duration; final blob size.
|
||||
6. NO `as any`; NO `@ts-ignore`. If a type narrowing is genuinely needed, use a typed predicate (`function isSimpleBlock(el: EBMLElementDetail): el is SimpleBlockElement {...}`).
|
||||
7. Run `npx vitest run tests/background/webm-remux.test.ts` — all 5 must flip GREEN.
|
||||
8. Run `npx tsc --noEmit` — must be 0.
|
||||
9. Run full suite — 11 files / 60 tests / 2 failed | 58 passed (the only remaining failures are the 2 webm-playback duration RED; those drive Task 4 GREEN once the call site swaps).
|
||||
DESIGN NOTE per project style: prefer if-else chains over early returns is RELAXED for guard clauses (the empty-input and null-trackInfo guards are clearer as `if (cond) return ...`). Document this exception in the file's header comment.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run tests/background/webm-remux.test.ts && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `src/background/webm-remux.ts` exists, exports `remuxSegments`, has extensive JSDoc per project style (block comment header + per-function docstrings).
|
||||
- All 5 tests in `tests/background/webm-remux.test.ts` GREEN.
|
||||
- `npx tsc --noEmit` exit 0.
|
||||
- Full suite: only the 2 webm-playback duration tests RED (they stay RED until the call site swap + fixture regeneration in Tasks 4-5).
|
||||
- No `as any`, no `@ts-ignore`, no `console.log` — uses `new Logger('Remux')` for diagnostics.
|
||||
- File size 120-300 LOC inclusive of comments (per debug session's estimate; if implementation balloons past 350 LOC, consider extracting EBML walk to a separate `webm-ebml-parser.ts` — but a single-file implementation is acceptable).
|
||||
</acceptance_criteria>
|
||||
<done>Pure helper module landed; unit invariants pinned GREEN; full-suite delta = 5 new GREEN; tsc clean.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 4: Swap mergeVideoSegments → remuxSegments at call site in src/background/index.ts; await the now-async merge.</name>
|
||||
<read_first>
|
||||
- src/background/index.ts lines 1-50 (imports + EmptyVideoBufferError class)
|
||||
- src/background/index.ts lines 385-505 (mergeVideoSegments definition + createArchive body)
|
||||
- src/background/index.ts lines 534-623 (saveArchive — confirms it already awaits createArchive)
|
||||
- src/background/webm-remux.ts (the new helper from Task 3)
|
||||
- tests/background/request-id-protocol.test.ts (asserts createArchive→saveArchive error-surface contract; the swap MUST preserve EmptyVideoBufferError throw on empty input)
|
||||
</read_first>
|
||||
<files>src/background/index.ts</files>
|
||||
<behavior>
|
||||
- `createArchive` calls `await remuxSegments(videoBufferResponse.segments)` instead of synchronous `mergeVideoSegments(...)`.
|
||||
- `mergeVideoSegments` function declaration is removed from the file (grep returns 0 hits).
|
||||
- `EmptyVideoBufferError` throw paths preserved on (a) zero segments AND (b) zero-byte merged blob.
|
||||
- All existing tests in tests/background/* still GREEN (the request-id-protocol contract tests don't care whether the merge is sync or async — they stub saveArchive at a higher level).
|
||||
</behavior>
|
||||
<action>
|
||||
1. RED step: rerun `npx vitest run` against current state — confirm the 2 RED webm-playback tests are still RED + remux unit tests GREEN. (No new failing test to write here; the gate is the existing webm-playback RED + the absence-of-mergeVideoSegments grep.)
|
||||
2. Add `import { remuxSegments } from './webm-remux';` near the top of `src/background/index.ts` (alphabetized with existing imports per project style).
|
||||
3. Replace the body of `createArchive`'s video-merge block (currently lines 444-456) with:
|
||||
```typescript
|
||||
if (videoBufferResponse.segments.length === 0) {
|
||||
throw new EmptyVideoBufferError(
|
||||
'no video segments available — buffer fetch returned empty (port replacement timed out, or recorder never started)',
|
||||
);
|
||||
}
|
||||
const videoBlob = await remuxSegments(videoBufferResponse.segments);
|
||||
if (videoBlob.size === 0) {
|
||||
throw new EmptyVideoBufferError(
|
||||
`remuxed video blob is zero bytes (segment count=${videoBufferResponse.segments.length})`,
|
||||
);
|
||||
}
|
||||
zip.file('video/last_30sec.webm', videoBlob);
|
||||
logger.log(`✓ Added video (remuxed): ${videoBlob.size} bytes`);
|
||||
```
|
||||
**W-01 fix (2026-05-16 checker pass): error message string change.**
|
||||
The EmptyVideoBufferError detail string changes from 'merged video blob is zero bytes' → 'remuxed video blob is zero bytes'. Before committing, pre-flight grep `src/` and `tests/` for the literal 'merged video blob is zero bytes' to confirm no downstream consumer matches on that string:
|
||||
```
|
||||
grep -RIn 'merged video blob is zero bytes' src/ tests/ || true
|
||||
```
|
||||
Empirical pre-check (executed 2026-05-16 from current tree): only `src/background/index.ts:452` carries the literal. `tests/background/request-id-protocol.test.ts` asserts on `error.code` ('empty-video-buffer'), NOT on the free-text message — so the rename is safe. Document the empty grep result in the commit message body so the diff reviewer sees the safety check ran.
|
||||
4. DELETE the entire `mergeVideoSegments` function declaration (currently lines 385-421 — including the JSDoc header explaining D-13 file-concat). Replace with a single comment line:
|
||||
```typescript
|
||||
// mergeVideoSegments (D-13 file-concat) retired in Plan 01-08 (D-14-remux):
|
||||
// see src/background/webm-remux.ts for the single-EBML remux path.
|
||||
```
|
||||
5. `createArchive` is already declared `async`; the new `await remuxSegments(...)` slots in without signature changes. Verify saveArchive's `const archiveBlob = await createArchive(...)` already awaits — no changes needed there.
|
||||
6. Run `npx tsc --noEmit` — exit 0.
|
||||
7. Run `npx vitest run`:
|
||||
- tests/background/webm-remux.test.ts: GREEN (Task 3)
|
||||
- tests/background/request-id-protocol.test.ts: GREEN (no contract change)
|
||||
- tests/background/port-lifecycle-continuous.test.ts: GREEN (no contract change)
|
||||
- tests/offscreen/webm-playback.test.ts: 2 of 4 still RED (the new duration assertions) — these stay RED until the FIXTURE is regenerated (Task 5).
|
||||
- All other tests: GREEN.
|
||||
8. Run `grep -nc 'mergeVideoSegments' src/background/index.ts` → must report `0` (or `0` lines with the function declaration; the deletion comment may or may not name it, your call).
|
||||
9. **B-01 fix (2026-05-16 checker pass): CONTEXT.md amendment provenance verification (folded in from retired Task 6).** The orchestrator already appended the D-14-remux amendment block to `.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md` at plan creation time (alongside D-15-display-surface, D-16-toolbar, D-17-onboarding for Plans 01-09 and 01-10). This step is idempotent grep verification — no file mutation expected. Run these four greps; ALL must return exactly one match:
|
||||
(a) `grep -c 'D-14-remux: WebM remux via ts-ebml + webm-muxer' .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md` → must be 1. (B-02 fix: anchor on the disambiguated `D-14-remux` marker, NOT bare `D-14` which would also match the historical `**D-14:** **Not applicable**` line in the original decisions block.)
|
||||
(b) `grep -c '^- \*\*D-13:\*\* \*\*Fallback if D-12 fails:\*\*' .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md` → must be 1. (Confirms the original D-13 text is intact.)
|
||||
(c) `grep -c 'Amendment .Phase 01-stabilize-video-pipeline, 2026-05-16. — D-17-port-lifecycle' .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md` → must be 1. (Confirms the prior D-17-port-lifecycle amendment block intact; note the disambiguated name per B-02.)
|
||||
(d) Run, verbatim (note: the literal contains backticks, so use single quotes around the -F argument exactly as shown — do NOT wrap this entire command in Markdown inline-code backticks when copy-pasting):
|
||||
|
||||
grep -cF '`src/background/webm-remux.ts` replaces' .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md
|
||||
|
||||
Must report exactly `1`. Confirms D-14-remux cites the new module (full path `src/background/webm-remux.ts`) replacing the old `mergeVideoSegments`. Anchor empirically verified to match CONTEXT.md line 412 (full path prefix is required; the un-prefixed form `webm-remux.ts` replaces` does not appear as a contiguous substring in the live file).
|
||||
If ANY of (a)–(d) returns zero, STOP and report to the orchestrator: the planner-time amendment was not committed or was clobbered downstream. Do NOT re-append a competing amendment block — debug the root cause first.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && npx vitest run --reporter=dot && test "$(grep -v '^\s*//' src/background/index.ts | grep -c 'mergeVideoSegments')" = "0" && grep -q 'D-14-remux: WebM remux via ts-ebml + webm-muxer' .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md && grep -q '^- \*\*D-13:\*\* \*\*Fallback if D-12 fails:\*\*' .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md && grep -q 'D-17-port-lifecycle' .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md && grep -qF '`src/background/webm-remux.ts` replaces' .planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `src/background/index.ts` imports `remuxSegments` and awaits it in `createArchive`.
|
||||
- `mergeVideoSegments` is deleted from the file (grep excluding comments returns 0).
|
||||
- `EmptyVideoBufferError` throw paths preserved per the request-id-protocol contract tests.
|
||||
- `npx tsc --noEmit` exit 0.
|
||||
- `npx vitest run` shows the same 2 webm-playback duration RED still failing (because the FIXTURE on disk is the old multi-EBML one — Task 5 regenerates it); every other test GREEN.
|
||||
- `npm run build` exit 0 (verifies the new import resolves through crxjs/Vite into the SW bundle without externalizing ts-ebml or webm-muxer — both are pure JS without native deps so the bundle should swallow them cleanly).
|
||||
- **B-01 fix: CONTEXT.md provenance chain verified intact via grep** — all four checks in action step 9 pass (D-14-remux present; D-13 original preserved; D-17-port-lifecycle amendment preserved; webm-remux.ts cited in D-14-remux body). No file mutation by this verification step; if any grep fails, the executor STOPS and escalates rather than re-appending.
|
||||
</acceptance_criteria>
|
||||
<done>Call site swapped to remux; legacy merge gone; saveArchive's empty-buffer error surface preserved; build clean; 2 RED tests now waiting only on the fixture regen; CONTEXT.md amendment provenance verified intact (D-14-remux / D-13 / D-17-port-lifecycle / webm-remux.ts citation all grep-confirmed).</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 5: Operator regenerates tests/fixtures/last_30sec.webm via ./smoke.sh against the post-remux build; confirms ~30 s Chrome + mpv playback.</name>
|
||||
<files>(operator-driven; no specific source file modified by this checkpoint)</files>
|
||||
<action>See <how-to-verify> below — operator-driven empirical check; the executor agent must not bypass this checkpoint by stubbing.</action>
|
||||
<verify>
|
||||
<automated>echo "checkpoint:human-verify — see how-to-verify section; resume signal is the gate"</automated>
|
||||
</verify>
|
||||
<done>Operator types "approved" after running the how-to-verify steps. See <resume-signal> for the exact gate.</done>
|
||||
<what-built>
|
||||
Tasks 1-4 landed: ts-ebml + webm-muxer installed, `remuxSegments()` implemented in `src/background/webm-remux.ts`, `mergeVideoSegments` deleted from `src/background/index.ts`, and `createArchive` now produces a single-EBML-headered WebM. The 2 RED tests in `tests/offscreen/webm-playback.test.ts` (lines 232-316) still fail because they read the committed fixture `tests/fixtures/last_30sec.webm` which was captured against the OLD D-13 file-concat build. This checkpoint regenerates the fixture against the new build.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Build: `npm run build` (must exit 0; produces `dist/`).
|
||||
2. Run smoke: `KEEP_PROFILE=0 ./smoke.sh`.
|
||||
3. In the launched Chrome window: Load Unpacked → select `dist/` → click the extension toolbar icon → screen-share picker auto-accepts the "Mokosh Smoke Test" tab.
|
||||
4. Wait at least 35 seconds (longer is fine — wait 5+ minutes if convenient to also re-validate the post-Option-C port lifecycle still holds across the 290 s mark which is now an irrelevant timeline anyway).
|
||||
5. Click the extension icon → "Сохранить отчёт об ошибке". The script will detect the new `session_report_*.zip`, extract `video/last_30sec.webm`, and stage it to `/tmp/mokosh-last_30sec.webm`.
|
||||
6. **Empirical playback check (the actual gate)** — open `/tmp/mokosh-last_30sec.webm` in:
|
||||
(a) Chrome (drag the file into a fresh tab) — observe playback duration in the controls; MUST be approximately 30 s (>= 25 s), NOT ~9 s.
|
||||
(b) mpv (`mpv /tmp/mokosh-last_30sec.webm`) — observe the duration shown in the title bar and the playback proceeds the full ~30 s without stopping early.
|
||||
7. Run `ffprobe -v error -show_entries format=duration -of csv=p=0 /tmp/mokosh-last_30sec.webm` and confirm the reported duration in seconds is between 25.0 and 30.0.
|
||||
8. If steps 6+7 PASS: copy `/tmp/mokosh-last_30sec.webm` over `tests/fixtures/last_30sec.webm` (`cp /tmp/mokosh-last_30sec.webm tests/fixtures/last_30sec.webm`), then run `npx vitest run tests/offscreen/webm-playback.test.ts` — all 4 tests MUST flip GREEN.
|
||||
9. If any of steps 6/7 FAIL: do NOT replace the fixture. Report the failure mode (duration value, mpv error, ffmpeg stderr) so Tasks 3-4 can be revised. The most likely failure modes are (a) webm-muxer's `frameRate` config disagreeing with the actual VP9 frame cadence → may need to derive from cluster timestamps; (b) keyframe-flag parsing off-by-one → first frame of each segment must be 'key', subsequent 'delta'; (c) CodecPrivate omission causing Chrome to fail to initialize the VP9 decoder; (d) base64 decode path failure: confirm via SW console 'Decoded buffer segment N: blob size=...' lines from decodeBufferSegments fired BEFORE remuxSegments runs; if those lines are missing, segments arrived as base64-string blobs and remux will silently garbage out (the SimpleBlock walk reads non-sensical bytes); (e) ts-ebml parse failure on first segment: width/height derive from the first segment's track info; if `pickTrackInfoFromSegment` returns null, remux falls back to 1024x768 OR throws — check SW console for 'Remux: pickTrackInfoFromSegment returned null' diagnostic, which signals the first segment did not contain a Tracks → TrackEntry → Video element tree (segment shape changed from what the d13 probe documented).
|
||||
</how-to-verify>
|
||||
<resume-signal>
|
||||
Type "approved" after step 8 lands (fixture committed and all 4 webm-playback tests GREEN). If step 9 hit, paste the failure diagnostic and the executor will iterate on Task 3.
|
||||
</resume-signal>
|
||||
</task>
|
||||
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| offscreen↔SW long-lived port (D-17-port-lifecycle architecture preserved) | base64-encoded VideoSegment bytes cross here; the SW deserializes via base64ToBlob and now hands them to remuxSegments. No new trust boundary introduced by this plan. |
|
||||
| third-party JS libraries (ts-ebml + webm-muxer) | NEW: untrusted code from npm gains access to VP9 frame bytes. Both are MIT-licensed pure-JS libraries with no native deps. Per the d13 debug session library survey: ts-ebml has a single `typeof window` self-fallback (safe); webm-muxer is zero-DOM-refs. Verified by `tests/background/webm-remux-deps.test.ts`. |
|
||||
| ffprobe + ffmpeg CLI invocations (existing — test-only) | Tests Task 2 and Task 5 invoke `/usr/bin/ffprobe` and `/usr/bin/ffmpeg`. Trust-boundary unchanged from existing tests/offscreen/webm-playback.test.ts pattern; binaries are system-installed and read-only. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-1-08-01 | Tampering | `src/background/webm-remux.ts:remuxSegments` consuming attacker-crafted WebM segments | accept | Defense in depth: the segments originate INSIDE the extension's own offscreen MediaRecorder; an attacker controlling them already controls the extension. Wrapping ts-ebml parse in try/catch and returning `new Blob([])` on parse failure (which surfaces as EmptyVideoBufferError downstream) provides a clean failure mode without any new trust surface. |
|
||||
| T-1-08-02 | Denial of Service | adversarial VideoSegment[] causing remuxSegments to allocate unbounded memory | accept | Buffer caller (saveArchive) already gates total input at 3 segments × ~800 KB = ~2.4 MB ceiling; ts-ebml + webm-muxer combined operate in pure-JS RAM without spawning sub-processes; SW heap headroom dominates the budget. |
|
||||
| T-1-08-03 | Information Disclosure | npm supply-chain compromise of ts-ebml or webm-muxer | mitigate | `package-lock.json` pins exact resolved hashes. Both libraries vendored at release versions verified live on npm 2026-05-16 (`ts-ebml@3.0.2`, `webm-muxer@5.1.4`). No `npm audit` warnings at install time per Task 1 acceptance. Phase 5 hardening may add an automated SCA check; out of scope here. |
|
||||
| T-1-08-04 | Repudiation | the regenerated fixture differing from operator's run-time recording shape | mitigate | Task 5 is the in-the-flesh empirical check by the operator (mpv + Chrome + ffprobe). The fixture commits ONLY after step 8 passes; if step 9 fails, the plan iterates rather than committing a misleading fixture. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `tests/offscreen/webm-playback.test.ts` shows 4 of 4 tests GREEN after Task 5 lands.
|
||||
- `tests/background/webm-remux.test.ts` shows 5 of 5 tests GREEN after Task 3.
|
||||
- `tests/background/webm-remux-deps.test.ts` shows 2 of 2 tests GREEN after Task 1.
|
||||
- `npx vitest run` final: 12 files / 60 tests / ALL GREEN.
|
||||
- `npx tsc --noEmit` exit 0.
|
||||
- `npm run build` exit 0.
|
||||
- `grep -c 'mergeVideoSegments' src/background/index.ts` excluding comments returns 0.
|
||||
- ffprobe on `tests/fixtures/last_30sec.webm` reports container duration >= 25_000 ms.
|
||||
- Operator empirical check: Chrome + mpv both play the fixture for ~30 s end-to-end.
|
||||
- CONTEXT.md ends with D-14-remux amendment (disambiguated marker per B-02 fix); original D-13 and D-17-port-lifecycle amendment blocks unmodified.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
SPEC §10 #7 (`last_30sec.webm` plays back in a browser) is functionally satisfied at the codebase level when:
|
||||
1. The regenerated `tests/fixtures/last_30sec.webm` plays for >= 25 s in Chrome AND mpv (operator-verified Task 5).
|
||||
2. The 4 webm-playback.test.ts assertions (zero decoder errors, no ended-prematurely, container duration >= 25 s, ffmpeg decode timeline >= 25 s) all GREEN.
|
||||
3. The remux helper unit tests (5) all GREEN against fixture-derived inputs.
|
||||
4. No file-concat merge path remains in the SW.
|
||||
5. The 53 baseline GREEN tests (Plan 01-07 closure suite + Option C tests) remain GREEN — no collateral regression.
|
||||
6. CONTEXT.md amendment block landed (D-14-remux disambiguated marker; verified by Task 4 step 9 grep checks).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md` using the standard template. Cite: the 2 RED tests that flipped GREEN, the deleted mergeVideoSegments + new remuxSegments file, the regenerated fixture size + ffprobe duration, the CONTEXT amendment landing, npm dep additions with pinned versions, the total bundle size delta (`npm run build` output before vs after).
|
||||
</output>
|
||||
Reference in New Issue
Block a user