Files
mokosh/.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md
Mark 3f8e31a329 feat(04-06): A35 live-DOM inline-SVG harness check + A17.8 raw-source update + back-patch
Closes the iter-2 BLOCKER 1 resolution end-to-end: the inline-SVG
strategy now has HONEST automated coverage at two layers — source
contract (Task 1 unit tests + the narrowed A17.8 source-bundling
grep) and live-DOM cascade (the NEW host-side A35 harness assertion
that opens welcome.html as a real Puppeteer tab).

- tests/uat/extension-page-harness.ts (A17.8 NARROWED HONESTLY):
  swap the data:image/svg+xml URL-grep + .svg filename-grep target
  for a raw-source grep — A17.8 now asserts the welcome chunk JS
  contains the raw SVG signature `stroke="currentColor"` AND the
  canonical `viewBox="0 0 32 32"` (the `?raw` import's output). The
  explanatory comment block now DISAVOWS the live-DOM claim and
  points at the NEW A35 driver for the runtime injection + cascade
  proof. A17.8 is honest source-bundling only.
- tests/uat/lib/harness-page-driver.ts (NEW host-side driveA35):
  appended LAST per the iter-2 ADV-2C concern (any driver-pollution
  worry is moot since nothing reads A35's return value, AND
  welcomePage.close() in finally guarantees no tab leak). driveA35
  opens chrome-extension://<id>/src/welcome/welcome.html in a fresh
  browser.newPage() tab, waits for the `.welcome-hero__mark svg`
  selector at DOMContentLoaded, then runs a single page.evaluate()
  that reads four signals: A35.1 inline <svg> present, A35.2
  stroke=currentColor, A35.3 getComputedStyle().stroke resolves to
  a non-default colour (the real cascade proof), A35.4 no legacy
  <img> in the slot. Host-side pattern mirrors driveA32/A33/A34.
- tests/uat/harness.test.ts (orchestrator wiring):
  + driveA35 added to the import block from './lib/harness-page-driver'.
  + driveA35Wrapped closure capturing handles.browser + handles.extensionId
    (alongside driveA33Wrapped/driveA34Wrapped).
  + { name: 'A35', drive: driveA35Wrapped } appended as the LAST
    entry of the `drivers` array. Total auto-increments via
    `drivers.length + 1` (line 580) — no hardcoded count to bump.
  + Architecture banner string (line 283) refreshed with A33, A34,
    A35 inline (ADV-2A cosmetic advisory — banner was already stale
    pre-04-06; A33+A34 added at the same time).
- .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md
  (back-patch, DEFECT 2 resolution):
  Flipped 5 lines (22, 47, 82, 135, 205) that carried the now-stale
  "deferred to Phase 5" framing for cursor visibility — the
  `cursor: 'always'` constraint was opportunistically shipped in
  Plan 01-09 (recorder.ts:285) and is verified by Plan 04-06 Task 1
  (tests/build/cursor-visibility.test.ts). Each flip is surgical
  (single line / single bullet, with explicit "back-patched in
  Phase 4 Plan 04-06" citation). Historical commit-description
  lines 40, 89, 109, 110 are LEFT unchanged — they describe what
  the Phase-1-closure commits literally did at the time, not
  forward-looking deferrals.
- .planning/phases/04-harden-clean-up-optional/deferred-items.md
  (correction, BLOCKER 2 resolution):
  Corrected the misdiagnosed entry from commit 6a989e8. The prior
  entry named tests/build/strict-meta-json-validation.test.ts as
  failing on a clean tree — that diagnosis was WRONG (the test is
  8/8 GREEN in isolation). The real root cause is the pre-existing
  04-CONTEXT #9 + #10 parallel-vitest / ffprobe-timeout flake
  family (lands non-deterministically on whichever ffprobe / race
  test loses the worker race; observed instance this session was
  tests/background/webm-remux.test.ts > ffprobe -count_frames,
  which is also 5/5 GREEN in isolation). True clean baseline is
  184/184 GREEN; 188/188 after Plan 04-06's +4 new tests.

Gates run:
- npx tsc --noEmit exit 0.
- npm run build:test exit 0; dist-test/assets/welcome-CMygHJ_J.js
  carries the raw SVG source.
- HEADLESS=1 SKIP_PROD_REBUILD=0 SKIP_LONG_UAT=1 npm run test:uat:
  36/36 UAT assertions GREEN (was 35/35; +A35). A17.8 PASS:
  currentColorStroke=true, canonicalViewBox=true. A35 live-DOM
  probe: svgPresent=true strokeAttr=currentColor
  computedStroke="rgb(250, 247, 241)" (linen-50, the
  --mks-fg-inverse value flowing through the cascade — the
  currentColor strategy WORKS in real Chrome) imgPresent=false.
- All Task 3 acceptance greps PASS: driveA35 count in
  harness-page-driver.ts=5, in harness.test.ts=6; name:'A35'=1;
  getComputedStyle=6; stroke="currentColor" in
  extension-page-harness.ts=4; data:image/svg+xml=0 (grep target
  and comment refs both removed).

References:
- 04-06-PLAN.md iter-2 BLOCKER 1 + BLOCKER 2 resolutions.
- .planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md
  §"Implementation amendment" (Option A currentColor + inline-SVG).
2026-05-26 08:48:43 +02:00

244 lines
26 KiB
Markdown
Raw Permalink 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
plan: 07
subsystem: video
tags: [webm, vp9, mediarecorder, getdisplaymedia, offscreen, ffprobe, ffmpeg, mv3, chrome-extension, ring-buffer, restart-segments]
# Dependency graph
requires:
- phase: 01-stabilize-video-pipeline
provides: Plans 01..06 — doc cascade (D-A1..D-A6), Wave-0 test infra (vitest + 4 RED tests), offscreen recorder TDD (D-09..D-11 originally, D-13 after fix-a3), port keepalive + OFFSCREEN_READY handshake, SW shrink (alarms/IDB/tabCapture deletion + port host), build pipeline collapse (vite.config.ts inline plugin deletion)
provides:
- D-12 ffprobe acceptance gate (structural validation of `tests/fixtures/last_30sec.webm`) — exit 0, zero stderr
- A3 empirical-playback acceptance gate (operator-confirmed Chrome playback + ffmpeg decoder dry-run exit 0, zero packet errors)
- `tests/fixtures/last_30sec.webm` regression fixture (1.6 MB VP9 1142×1038 captured against the D-13 restart-segments recorder)
- REQ-video-ring-buffer marked Complete; SPEC §10 acceptance criteria #2, #3, #7 functionally green at Phase 1 level
- D-09..D-11 RETIRED in favor of D-13 restart-segments (mid-phase architectural correction, both pre-staged fallbacks activated)
- CON-webm-header-retention RETIRED (D-13's per-segment header isolation makes it meaningless)
affects:
- Phase 2 (DOM + event-capture privacy) — capture pipeline is now always-on regardless of active tab; Phase 2 content scripts MUST NOT add competing keepalives (port `'video-keepalive'` keeps SW alive)
- Phase 3 (export pipeline + popup state machine) — popup will consume `getSegments()` output via the existing SW port host
- Phase 4 (SPEC §10 smoke verification) — Phase 4 verifies the full 9-criterion sweep; Phase 1 only proved #2/#3/#7
- `getDisplayMedia` cursor visibility constraint (`video: { cursor: 'always' }`) — opportunistically shipped Plan 01-09 (recorder.ts:285); verified Phase 4 Plan 04-06 via tests/build/cursor-visibility.test.ts. The original "Phase 5" framing here is back-patched stale; see Phase 4 Plan 04-06 closure.
- GSD framework retro candidate — auto-injection of empirical-acceptance gates when RESEARCH.md flags HIGH-risk assumptions
# Tech tracking
tech-stack:
added: [ffprobe + ffmpeg (system CLIs used for the D-12 / A3 gates)]
patterns:
- "Pre-staged contingency activation: when RESEARCH.md flags an assumption as HIGH-risk, plan-time pre-stages a fallback skeleton (D-13 commented block in recorder.ts; base64 wire-format research in CONTEXT.md Doc Cascade). The smoke step then acts as an A/B gate: if simple-approach fails empirically, the executor activates the pre-staged fallback in-cycle rather than scheduling a new plan."
- "Three-attempt acceptance pattern: attempt 1 (single-continuous recorder) failed at binary-transfer layer → D-12 fix; attempt 2 (after D-12) passed ffprobe but failed Chrome playback → A3 fix via D-13 activation; attempt 3 clean. Each failure was contained by its pre-staged fallback."
key-files:
created:
- "tests/fixtures/last_30sec.webm — 1.6 MB regression fixture against the D-13 recorder (replaces stale fixture from 87909d9)"
- ".planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md — this file"
- ".planning/debug/resolved/d12-blob-port-transfer-fails.md — D-12 debug session, resolved"
- ".planning/debug/resolved/webm-playback-freeze.md — A3 debug session, resolved"
modified:
- ".planning/REQUIREMENTS.md — REQ-video-ring-buffer marked Complete; description AMENDED to reflect D-13 restart-segments"
- ".planning/ROADMAP.md — Phase 1 row Complete; Success Criteria checked off; cursor-visibility refinement appended to Phase 5"
- ".planning/STATE.md — Phase 1 closure block + frontmatter flip to phase_complete + Decisions log appended"
key-decisions:
- "D-12 acceptance gate (ffprobe) and A3 empirical-playback gate (operator + ffmpeg dry-run) treated as TWO distinct gates. The original Plan 07 conflated them under 'ffprobe-clean'; the A3 debug session proved they are independent. Phase 1 closure required BOTH green."
- "Plan 07 closure is auto-handled (Task 2 of original PLAN.md) — the checkpoint:human-verify returned `approved`; the auto-closure flips REQ-video-ring-buffer + STATE + ROADMAP in one atomic commit and writes this SUMMARY in another."
- "D-09..D-11 retirement is permanent. The original D-13 'fallback if D-12 fails' framing now stands as the production approach; CONTEXT.md decisions remain on record for SPEC provenance but the executable surface is restart-segments."
- "Cursor visibility (`video: { cursor: 'always' }`) was opportunistically shipped in Plan 01-09 at src/offscreen/recorder.ts:285 and verified in Phase 4 Plan 04-06 (tests/build/cursor-visibility.test.ts node-env regression pin). The Phase 1 closure correctly did not back-patch it into Phase 1 itself — Phase 1 was a stabilization phase, not a UX-refinement phase; the original 'deferred to Phase 5' framing in this SUMMARY was made stale by the Plan 01-09 opportunistic landing, and is back-patched here in Phase 4 Plan 04-06."
- "Phase 1 closure satisfies SPEC §10 #2, #3, #7 functionally — but the canonical §10 sweep is owned by Phase 4. This avoids confusing 'Phase 1 closed' with 'SPEC §10 complete'; the latter requires the full 9-criterion smoke under Phase 4."
patterns-established:
- "Multi-EBML-header WebM concatenation is acceptable for the SPEC §10 #7 'plays back in a browser' criterion. Chrome's MSE pipeline handles it natively. The ffmpeg `-f null` decode dry-run produces muxer DTS-monotonicity warnings at segment join boundaries; these are NOT decoder errors and are documented as an expected D-13 trade-off — not a failure."
- "Auto-closure of a checkpoint-gated plan: when a `checkpoint:human-verify` returns `approved`, a follow-up automation task (Task 2 of the same plan) lands the closure side-effects (REQ flip, STATE update, ROADMAP update, SUMMARY write). This pattern keeps the checkpoint task minimal and the closure ceremony atomic."
- "Pre-staged fallbacks pay for their planning cost in the smoke step. Two-out-of-two HIGH-risk-flagged assumptions in Phase 1 (binary-transfer wire format under MV3 port serialization + VP9 keyframe orphan-P-frame risk under age-trim) failed empirically as predicted and were resolved by their pre-staged fallbacks without re-planning."
requirements-completed: [REQ-video-ring-buffer]
# Metrics
duration: "~10min (Plan 07 closure ceremony); not counting the two debug sessions (~30 min D-12, ~45 min A3) which are accounted for in their respective resolved-session files"
completed: 2026-05-15
---
# Phase 1 Plan 07: Manual Smoke + ffprobe + Empirical Playback Acceptance Gates Summary
**Two-gate phase closure (D-12 ffprobe structural + A3 empirical Chrome playback) green against `tests/fixtures/last_30sec.webm` after a three-attempt journey through two pre-staged fallback activations; REQ-video-ring-buffer complete, Phase 1 closed.**
## Performance
- **Duration:** ~10 min (closure ceremony only — three atomic commits + SUMMARY write). Plan 07's total elapsed time across all three smoke attempts was ~3.5 hours including the two debug sessions (D-12 ~30 min + A3 ~45 min); those are accounted for in the respective `.planning/debug/resolved/*.md` files.
- **Started:** 2026-05-15T21:40:00Z (this closure session, after operator returned `approved` on the checkpoint:human-verify)
- **Completed:** 2026-05-15T21:43:00Z (approx — three closure commits landed sequentially)
- **Tasks:** 2 (Task 1 = `checkpoint:human-verify` → operator returned `approved`; Task 2 = AUTOMATED closure with REQ flip + STATE update + SUMMARY)
- **Files modified:** 4 (fixture + REQUIREMENTS.md + ROADMAP.md + STATE.md) plus this SUMMARY created
## Accomplishments
- **D-12 ffprobe gate GREEN.** `ffprobe -v error -f matroska -i tests/fixtures/last_30sec.webm` returns exit 0 with empty stderr. ffprobe `-show_streams` confirms valid VP9 codec, Profile 0, 1142×1038 frame, time_base 1/1000, start_pts 0, color space bt709.
- **A3 empirical-playback gate GREEN.** Operator confirmed end-to-end Chrome playback clean — no ~1 s freeze. `ffmpeg -v warning -i tests/fixtures/last_30sec.webm -f null -` exits 0 with zero decoder errors; the only warnings are muxer DTS-monotonicity reports at segment join boundaries (expected: D-13 concatenates three self-contained multi-EBML-header WebMs; Chrome's MSE pipeline handles this natively, satisfying SPEC §10 #7 "plays back in a browser").
- **All 30/30 unit tests green** across 8 test files (`tests/offscreen/{codec-check,handshake,port,port-serialization,ring-buffer,segment-keyframes,segment-rotation,webm-playback}.test.ts`). Both empirical ffmpeg dry-runs in `webm-playback.test.ts` flipped GREEN after the fresh fixture committed in cd61cbc — previously RED against the stale 87909d9 fixture.
- **`npx tsc --noEmit` clean** at closure time; `npm run build` clean (verified during the fix-a3 cycle, unchanged since).
- **REQ-video-ring-buffer marked Complete** in REQUIREMENTS.md (checkbox + traceability table row); ROADMAP Phase 1 row marked Complete 2026-05-15.
- **D-09..D-11 retired permanently** in favor of D-13 restart-segments; CON-webm-header-retention also retired. Captured in REQUIREMENTS.md amendment + STATE.md Decisions log.
- **Cursor-visibility refinement: opportunistically shipped in Plan 01-09 (recorder.ts:285 `cursor: 'always'`); verified in Phase 4 Plan 04-06** via `tests/build/cursor-visibility.test.ts` (node-env regression pin) + the operator-empirical SAVE flow showing the pointer visible in `video/last_30sec.webm`. The original 2026-05-15 "deferred to Phase 5" framing was correct at Phase 1 closure but was made stale by the Plan 01-09 opportunistic landing — back-patched in Phase 4 Plan 04-06 Task 3.
## Task Commits
Plan 07 closure ceremony (this SUMMARY's three atomic commits):
1. **Task 1.5 (post-checkpoint): Fixture regeneration commit**`cd61cbc` (test): replaced the stale 87909d9 fixture with the fresh D-13-recorder capture (1.6 MB VP9 1142×1038).
2. **Task 2a: Phase 1 closure flips**`7df72aa` (feat): REQUIREMENTS.md REQ-video-ring-buffer flipped to Complete; ROADMAP.md Phase 1 row Complete + Success Criteria checked off + Phase 5 cursor-visibility appended; STATE.md Phase 1 Closure Notes block added + frontmatter to `phase_complete` + Decisions log entry.
3. **Task 2b: SUMMARY write**`<commit-3-hash>` (docs): this SUMMARY committed.
Plan 07 also retained two prior debug-session arbitration cycles (already on the branch — they ARE the substance of Plan 07's execution, but each was committed as its own resolved-session bundle, not as Plan 07 commits per se):
- **D-12 fix cycle (5 commits):** c0d9166, d653283, 2831849, d5bb948, bf07619 — base64 wire-format binary transport.
- **A3 fix cycle (6 commits):** 5530292, 6a1a034, 670daa3, f81438d, 87909d9, 872f25d — D-13 restart-segments activation.
_Note: Plan 07's original spec called for ONE commit at Task 2 completion. The closure ceremony was split into three atomic commits (fixture / REQ-STATE-ROADMAP flip / SUMMARY) so the SUMMARY can cite the prior commits by hash and so the REQUIREMENTS/ROADMAP/STATE flip is reviewable independently of the SUMMARY's narrative._
## Files Created/Modified
**Created (this closure session):**
- `tests/fixtures/last_30sec.webm` — replaces the stale 87909d9 fixture; 1.6 MB; VP9 1142×1038; captured against the D-13 restart-segments recorder; concat of 3 × ~10 s self-contained WebM segments
- `.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` — this file
**Modified (this closure session):**
- `.planning/REQUIREMENTS.md` — REQ-video-ring-buffer checkbox + description AMENDED + traceability row flipped to Complete
- `.planning/ROADMAP.md` — Phase 1 list-item + Success Criteria + Plan 07 row + Progress table row all flipped to Complete with 2026-05-15 date; Phase 5 P1/P2 list appended with `getDisplayMedia` cursor visibility refinement
- `.planning/STATE.md` — frontmatter `status` to `phase_complete`, completed_phases 0→1, completed_plans 6→7, percent 86→100; stopped_at + last_activity rewritten; Current Position rewritten for Phase 2 readiness; Performance Metrics Phase 1 row populated + Plan 07 row added; Decisions log appended with `[Phase 01-07-closure]` + `[Phase 01-07-deferred-to-5]`; Session Continuity rewritten; **Phase 1 Closure Notes** section added
**Created in prior fix cycles (referenced for context, not in this closure):**
- `src/shared/binary.ts` — D-12 fix: portable Blob ↔ base64 helpers
- `tests/offscreen/port-serialization.test.ts` — D-12 fix: 6 tests proving JSON serialization corruption + base64 round-trip pin
- `tests/offscreen/segment-keyframes.test.ts` — A3 fix: production-driven RED block (now GREEN) + documentation + GREEN-pinning blocks
- `tests/offscreen/segment-rotation.test.ts` — A3 fix: 8 tests pinning D-13 ring-buffer invariants
- `tests/offscreen/webm-playback.test.ts` — A3 fix: 2 empirical ffmpeg dry-runs (RED until fresh fixture, GREEN after cd61cbc)
- `.planning/debug/resolved/d12-blob-port-transfer-fails.md`
- `.planning/debug/resolved/webm-playback-freeze.md`
**Modified in prior fix cycles:**
- `src/shared/types.ts` — D-12: added `TransferredVideoChunk`; A3: renamed → `TransferredVideoSegment` + field renames + `isFirst` dropped
- `src/offscreen/recorder.ts` — D-12: base64 encode side; A3: D-13 restart-segments activation (the main rewrite, replacing D-09..D-11)
- `src/background/index.ts` — D-12: base64 decode side; A3: segment-semantics adaptation + `mergeVideoSegments`
- `tests/offscreen/ring-buffer.test.ts` — A3: retired to a vestigial breadcrumb after segment-rotation supersedes it
## Decisions Made
1. **Plan 07 closure is split into three atomic commits.** The original spec called for one commit at Task 2 completion. Splitting (fixture / REQ+STATE+ROADMAP flip / SUMMARY) makes each commit reviewable in isolation and lets the SUMMARY cite the fixture and closure commits by hash. Trade-off: three commits where one was specced — judged a worthwhile diff-clarity improvement.
2. **D-12 + A3 are treated as TWO distinct gates.** The original PLAN.md framed acceptance as "ffprobe-clean" (a single gate). The A3 debug session proved that a ffprobe-clean file can still freeze in Chrome playback — they are independent. Phase 1 closure requires both green, and the SUMMARY documents both gates explicitly so future phases can't conflate them.
3. **D-09..D-11 retirement is permanent.** The original D-13 framing in CONTEXT.md was "fallback if D-12 fails." After A3 activated it, D-13 IS the production approach. CONTEXT.md retains D-09..D-11 for SPEC provenance, but REQUIREMENTS.md, ROADMAP.md, and the executable surface (src/offscreen/recorder.ts) all reflect D-13 as authoritative.
4. **The muxer DTS-monotonicity warnings are NOT a failure.** ffmpeg `-f null -` prints warnings at segment join boundaries because the three concatenated WebM segments have overlapping per-segment timestamps (each segment's pts restarts at ~0 in its own EBML segment). This is the documented D-13 trade-off for multi-EBML-header WebM concat. SPEC §10 #7 only requires "plays back in a browser" — Chrome's acceptance is sufficient and was operator-confirmed.
5. **Cursor visibility refinement: at Phase 1 closure deferred (Phase 1 was a stabilization phase; adding `cursor: 'always'` mid-closure would have widened the diff beyond the closure ceremony). The constraint was opportunistically shipped in Plan 01-09 (recorder.ts:285) and verified in Phase 4 Plan 04-06.** The historical Phase-1-closure framing (deferral was correct at the time) is preserved as a point in the audit trail; the actual constraint landed earlier than planned via opportunistic refinement — back-patched here in Phase 4 Plan 04-06.
## Deviations from Plan
### Deviations from the ORIGINAL Plan 07 spec
**1. [Rule 4 → architectural escalation, then auto-resolved] D-12 binary transfer failure surfaced during first smoke attempt**
- **Found during:** Task 1 first attempt (operator's first ./smoke.sh run)
- **Issue:** Saved `last_30sec.webm` was 75 bytes of literal `"[object Object]"` text — chrome.runtime.Port JSON-serializes payloads across extension contexts, corrupting Blob → `{}``"[object Object]"` in the SW-side Blob ctor
- **Fix:** Pre-staged fallback activated — added `src/shared/binary.ts` with portable Blob ↔ base64 helpers; encoded on the offscreen side (`REQUEST_BUFFER` handler), decoded on the SW side (`BUFFER` receive)
- **Files modified:** `src/shared/binary.ts` (new), `src/shared/types.ts`, `src/offscreen/recorder.ts`, `src/background/index.ts`
- **Verification:** 6 new tests in `port-serialization.test.ts` (including a byte-exact reproduction of the 75-byte payload + a GREEN-pinning block for the base64 wire format); empirical re-smoke after fix produced a 2.1 MB ffprobe-valid WebM
- **Committed in:** c0d9166, d653283, 2831849, d5bb948, bf07619
- **Resolved-session doc:** `.planning/debug/resolved/d12-blob-port-transfer-fails.md`
**2. [Rule 4 → architectural escalation, then auto-resolved] A3 cluster-alignment freeze surfaced during second smoke attempt**
- **Found during:** Task 1 second attempt (operator's re-smoke after D-12 fix landed)
- **Issue:** The ffprobe-valid WebM froze ~1 s into Chrome playback. ffmpeg `-v warning -i ... -f null -` showed 8× `Error submitting packet to decoder: Invalid data found` + `File ended prematurely at pos. 2100851`. Root cause: D-09..D-11's age-trim evicted middle chunks containing VP9 keyframes that subsequent retained tail chunks depended on (orphan P-frames)
- **Fix:** Pre-staged fallback activated — the D-13 restart-segments skeleton commented at the bottom of `src/offscreen/recorder.ts` (lines 298-316 originally) was activated. Recorder lifecycle replaced: stop()/start() every 10 s on the same MediaStream, keep last 3 self-contained ~10 s WebM segments (3 × 10 s = 30 s preserved). Each segment is independently decodable. As a side effect, per-segment `.stop()` also fixed the "File ended prematurely" Matroska finalization gap
- **Files modified:** `src/offscreen/recorder.ts` (rewrite), `src/background/index.ts` (segment semantics + `mergeVideoSegments`), `src/shared/types.ts` (`TransferredVideoChunk``TransferredVideoSegment` + field renames + `isFirst` dropped), `tests/offscreen/ring-buffer.test.ts` (retired), `tests/offscreen/segment-rotation.test.ts` (new)
- **Verification:** 28/30 tests green immediately after the fix; the 2 remaining REDs were the empirical ffmpeg dry-runs against the still-stale fixture, which flipped GREEN once the fresh fixture committed in cd61cbc; operator-confirmed end-to-end Chrome playback clean
- **Committed in:** 5530292, 6a1a034, 670daa3, f81438d, 87909d9, 872f25d
- **Resolved-session doc:** `.planning/debug/resolved/webm-playback-freeze.md`
**3. [Closure ceremony split into 3 commits, not 1]** See Decisions Made #1 above.
---
**Total deviations:** 2 architectural escalations (both pre-staged fallback activations), 1 closure-ceremony commit-split. Both architectural escalations were anticipated in CONTEXT.md and RESEARCH.md (HIGH-risk flags); both pre-staged fallbacks activated cleanly without needing a fresh phase plan. The commit-split was a Plan 07 spec deviation but improves reviewability.
**Impact on plan:** The two pre-staged fallbacks paid for their planning cost in the smoke step exactly as designed. Phase 1's overall structure held; no plan re-numbering was needed; no `/gsd-insert-phase` was called. The closure ceremony itself is on-spec modulo the 3-commit split.
## Issues Encountered
Three smoke attempts, two pre-staged fallback activations, one clean acceptance.
**Attempt 1 (single-continuous MediaRecorder, originally specced under D-09..D-11):**
- Result: 75-byte text file in the archive (5 × `"[object Object]"`)
- Root cause: D-12 — chrome.runtime.Port JSON-serializes payloads, corrupting Blobs
- Resolution: Activate the base64 wire-format pre-stage from CONTEXT.md Doc Cascade
**Attempt 2 (D-12 fix landed, single-continuous recorder retained):**
- Result: 2.1 MB ffprobe-valid WebM; opens in Chrome but freezes ~1 s in
- Root cause: A3 — VP9 keyframe orphan-P-frame issue under age-trim eviction of middle chunks
- Resolution: Activate the D-13 restart-segments pre-stage from CONTEXT.md
**Attempt 3 (D-12 + A3 fixes both landed, D-13 recorder):**
- Result: 1.6 MB WebM (3 × ~10 s self-contained segments concatenated); ffprobe exit 0; ffmpeg dry-run exit 0 with zero decoder errors; operator-confirmed clean Chrome playback end-to-end
- Resolution: Commit the fresh fixture as `tests/fixtures/last_30sec.webm`; close Phase 1
## Process Observation — Candidate for GSD Framework Retro
**Plan 01-07's smoke step ended up surfacing TWO unanticipated-cascade failures (D-12 binary transfer + A3 keyframe alignment), each of which had a pre-staged fallback that activated cleanly without needing a fresh phase or replan.**
This is a real datapoint for the GSD framework. Two observations:
1. **Pre-staging works.** RESEARCH.md flagged both binary-transfer semantics under MV3 port serialization AND VP9 keyframe orphan-P-frame risk under age-trim as HIGH-risk assumptions. CONTEXT.md and PLAN.md pre-staged fallbacks for both (the Doc Cascade for binary transfer; the D-13 commented skeleton for keyframe alignment). Both HIGH-risk assumptions failed empirically, exactly as flagged. Both pre-staged fallbacks activated within ~30-45 min of detection. No re-planning was required.
2. **Plan 07's smoke step DID the empirical-acceptance-gate work that was, in spirit, what RESEARCH.md was warning about.** The smoke step is supposed to be a confirmation gate; it ended up being a discovery gate twice in a row.
**Candidate retro question for `/gsd-plan-phase`:** When RESEARCH.md flags an assumption as HIGH-risk AND a fallback is pre-staged, should the planner automatically inject an empirical-acceptance gate (e.g. ffmpeg dry-run + Chrome playback for video/codec assumptions; cross-context message round-trip test for MV3 messaging assumptions) BEFORE the phase's final commit-merge, rather than discovering the failure during the human-verify smoke step? The current sequence (Plan 01 → ... → 07 = smoke → debug session if smoke fails) works, but a slightly tighter feedback loop ("if smoke would surface a HIGH-risk-flagged failure, escalate to the pre-staged fallback BEFORE creating a debug session") might shorten the orchestration overhead in future phases that have similarly cascade-prone subject matter (codec / wire-format / container-spec work in particular).
Not a process bug — a possible process refinement. Logged here for `/gsd-plan-phase` retro consideration in Phase 2 or beyond.
This is the SECOND debug session in Phase 1's life — both written up in `.planning/debug/resolved/`. The cycle latency between "manual smoke reveals the issue" and "RED test in place + fix landed" was ~30 minutes for D-12 and ~45 minutes for A3. Acceptable but worth tightening.
## User Setup Required
None — Phase 1 ships as a Chrome extension with `desktopCapture` permission; the operator interacts with `chrome://extensions` "Load unpacked" for development and Chrome's native screen-share picker for capture. No external services configured by Phase 1.
The cursor visibility refinement added a `cursor: 'always'` constraint to the `getDisplayMedia` call in `src/offscreen/recorder.ts` (line 285) — no user action was required; the one-line code change shipped in Plan 01-09 and was verified in Phase 4 Plan 04-06. (Back-patched: the original Phase-1-closure framing said "Phase 5"; the actual landing was earlier.)
## Next Phase Readiness
**Ready for Phase 2:** The offscreen now owns capture entirely. Phase 2 (DOM + event-capture privacy via rrweb v2 maskInputFn + content-script input logging) plugs into the existing content-script architecture and does NOT touch the offscreen.
**Constraints Phase 2 must respect:**
- The port `'video-keepalive'` keeps the SW alive. Phase 2's content-script work MUST NOT add competing keepalives (e.g., redundant `chrome.alarms`, redundant ports). Audit P1 #8 (alarms keepalive ineffective) was resolved in Phase 1 by deleting `chrome.alarms`; do not re-introduce it.
- The SW's `onMessage` listener validates `sender.id === chrome.runtime.id` (T-1-04 SW-side sender check, documented 4 places in `src/offscreen/recorder.ts`). Content scripts have the same extension ID and pass this check. If Phase 2 adds a new sender path (e.g., an injected page-world script via `chrome.scripting`), Phase 2 must respect the guard.
- `mergeVideoSegments` in `src/background/index.ts` IS the export path for video. Phase 2 work goes into `events.json` / `session.json`; no merge function changes needed.
**Workflow next steps (per user settings: deep code-review + verifier enabled):**
1. `/gsd-code-review` (deep code-review gate on the Phase 1 branch) — covers all 25-ish Phase 1 commits including the two debug-session fix cycles + this closure ceremony.
2. `/gsd-verify-work` (verifier — runs the full vitest + tsc + grep gates against the Phase 1 branch).
3. After both pass: merge Phase 1, plan Phase 2 via `/gsd-plan-phase 2`.
**Blockers / concerns:** None. Phase 1 is complete; the two debug sessions are documented and resolved; pre-staged fallbacks worked as designed.
## Self-Check: PASSED
Verified after writing this SUMMARY:
- `tests/fixtures/last_30sec.webm` exists (1 633 459 bytes) ✓
- `.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` exists (this file) ✓
- Commit `cd61cbc` (fixture replacement) in `git log --all`
- Commit `7df72aa` (closure flips) in `git log --all`
- `grep -c "Phase 1 closure" .planning/STATE.md` → 3 (≥ 1) ✓
- `grep "REQ-video-ring-buffer" .planning/REQUIREMENTS.md` shows `[x]`
- `grep "Complete" .planning/ROADMAP.md` shows Phase 1 row Complete 2026-05-15 ✓
- `npx vitest run` → 30/30 green ✓
- `npx tsc --noEmit` → exit 0 ✓
- ffprobe + ffmpeg dry-run both exit 0 ✓
---
*Phase: 01-stabilize-video-pipeline*
*Completed: 2026-05-15*