diff --git a/.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md b/.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md new file mode 100644 index 0000000..e0444c5 --- /dev/null +++ b/.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md @@ -0,0 +1,1394 @@ +# Phase 4: Harden + clean up (optional) - Pattern Map + +**Mapped:** 2026-05-21 +**Files analyzed:** ~30 anticipated (6 new tests + 5 src/* + 4 build/config + 5 harness + 5 docs + 1 SUMMARY back-patch + 1 VERIFICATION.md NEW) +**Analogs found:** all anticipated files mapped (most exact; 2 role-match; 1 new pattern for stopServiceWorker CDP helper) +**Phase character:** Hardening — small surgical fixes across multiple subsystems + 1 new VERIFICATION.md. Approach B harness extension precedent (Plan 02-04 + Plan 03-01..04) is canonical for A33+ work. + +## File Classification + +### A. New unit tests (Wave 0 RED-first per `feedback-gsd-ceremony-for-fixes.md`) + +| New File | Role | Data Flow | Closest Analog | Match Quality | +|----------|------|-----------|----------------|---------------| +| `tests/build/no-new-function-in-sw-chunk.test.ts` | test (build-gate; dist/ grep) | file-I/O (build dist + recursive walk + substring count) | `tests/build/no-remote-fonts.test.ts` (Plan 01-12) | EXACT | +| `tests/build/dead-code-grep.test.ts` | test (build-gate; src/ grep) | file-I/O (src/ + vite.config.ts substring grep) | `tests/build/no-remote-fonts.test.ts` (Plan 01-12) — same scaffold, src/ scope instead of dist/ | EXACT | +| `tests/build/cursor-visibility.test.ts` (optional pin per RESEARCH §"Wave 0 Gaps") | test (src/ grep regression pin) | file-I/O (read `src/offscreen/recorder.ts:285` + assert literal `cursor: 'always'` present) | `tests/build/no-remote-fonts.test.ts` (Plan 01-12) — same scaffold, single-file positive assertion | EXACT (inverted polarity: presence not absence) | +| `tests/content/fetch-interception.test.ts` (NEW dir + file) | test (content-script unit; SUT in isolation) | request-response (fake `Request` + string-URL args; assert URL extraction) | `tests/background/start-video-capture-no-tab.test.ts` (Plan 01-09) — pure-function-with-stub pattern | ROLE-MATCH (different SUT location: src/content/ vs src/background/; same vi.fn stub-then-import scaffold) | +| `tests/content/navigation-tracking.test.ts` (NEW dir + file) | test (content-script unit; module-level state) | event-driven (synthetic `popstate`/`hashchange`; assert `previousUrl` tracking) | `tests/background/start-video-capture-no-tab.test.ts` (Plan 01-09) + `tests/background/onboarding.test.ts` (Plan 01-10 — module-level state assertion shape) | ROLE-MATCH | +| `tests/content/rrweb-timestamps.test.ts` (NEW dir + file) | test (content-script unit; timestamp semantics) | event-driven (rrweb emit; assert `Date.now()`-class epoch not page-load-relative) | `tests/background/start-video-capture-no-tab.test.ts` (Plan 01-09) — vi.fn + module-import scaffold | ROLE-MATCH | +| `tests/welcome/inline-svg.test.ts` (NEW dir + file) | test (welcome unit; DOM injection) | request-response (DOMParser parse + appendChild; assert `` injected with `currentColor`) | `tests/background/onboarding.test.ts` (Plan 01-10) — welcome-page side-effect assertion shape + `tests/i18n/manifest-i18n.test.ts` (Plan 01-12) — file-read + string assertion shape | ROLE-MATCH (combines the two: vi.fn + module-import + JSDOM-style assertion) | + +### B. Source code edits (production behavior fixes) + +| Modified File | Role | Data Flow | Closest Analog (for the pattern of change) | Match Quality | +|---------------|------|-----------|--------------------------------------------|---------------| +| `src/content/index.ts:147` (P1 #11 fetch URL extraction) | content script (interception wrapper) | request-response (window.fetch wrapped) | self-analog: `src/content/index.ts:167` existing `originalFetch.apply(this, args)` block | EXACT (1-line type-narrow within existing wrapper) | +| `src/content/index.ts:99-109` (P1 #14 navigation URL tracking) | content script (event listener + module state) | event-driven (popstate/hashchange listener) | self-analog: existing `let rrwebEvents` / `let userEvents` module-level state at `src/content/index.ts:21-24` | EXACT (add 1 module-level `let previousUrl`; mutate in handler) | +| `src/content/index.ts:36-42` (P1 #15 rrweb timestamps semantics) | content script (rrweb emit wrapper) | event-driven (rrweb emit callback) | self-analog: existing `event.timestamp = Date.now()` at `addUserEvent` line 54 — same epoch convention | EXACT (mirror UserEvent epoch convention into rrweb emit path) | +| `src/shared/brand/mokosh-mark.svg` (UI-SPEC stroke recolor) | brand asset (SVG markup) | static markup | self-analog: existing root `` element at line 2 — single attribute swap | EXACT (`stroke="#181b2a"` → `stroke="currentColor"`) | +| `src/welcome/welcome.ts:46 + 159-179` (UI-SPEC `?raw` import + DOMParser inline injection) | welcome page (asset injection) | request-response (Vite raw import + DOMParser parse + appendChild) | self-analog: existing `populateMark()` at lines 159-179 — replace `` with parsed `` while preserving aria + class + slot semantics | EXACT (rewrite of populateMark; preserve filter-pipeline form per project rule) | +| `src/welcome/welcome.css:91-95` (UI-SPEC `.welcome-hero__mark-img` selector update) | welcome page (CSS) | static styling | self-analog: existing `.welcome-hero__mark-img` rule at lines 91-95 — add `svg.welcome-hero__mark-img` sibling OR rely on CSS `color` inheritance | EXACT (selector broadening) | +| `globals.d.ts` (UI-SPEC `*.svg?raw` ambient decl) | type declaration | declarations | self-analog: existing `declare module '*.svg?url'` at lines 34-37 — copy-paste 4-line block with `?raw` suffix | EXACT (mirror existing ambient module decl) | +| `src/background/index.ts` (top-of-module setimmediate polyfill prelude per RESEARCH Q1) | SW entry (polyfill prelude) | static init | self-analog: existing top-of-module imports at lines 1-17; new 4-line `if (typeof globalThis.setImmediate === 'undefined')` block inserted before the long-form import block | EXACT (additive prelude; no existing code displaced) | +| `vite.config.ts` (RESEARCH Q1 `exclude: ['setimmediate']`) | bundler config | declarative config | self-analog: existing `nodePolyfills({ include: ['buffer'], globals: {...} })` at lines 14-22 — add 1 array property `exclude: ['setimmediate']` | EXACT (1-line addition to existing config block) | +| `src/content/index.ts` (cursor visibility — VERIFICATION ONLY per RESEARCH Finding 4; no edit) | content script (constraint check) | n/a (verification only) | N/A — grep gate only; cursor: 'always' already shipped at `src/offscreen/recorder.ts:285` per Plan 01-09 | VERIFICATION-ONLY | +| `generate-icons.js` → `generate-icons.cjs` (RESEARCH SC #3) | build script (CJS rename) | file-I/O (Node `require('fs')`) | self-analog: existing line 1 `const fs = require('fs')` is the CJS smoking-gun under `"type": "module"`; rename file extension fixes per Node docs | EXACT (single-file rename; no code change) | + +### C. Harness extensions (Approach B; A33+ continues A29-A32 sequence) + +| Modified File | Role | Data Flow | Closest Analog | Match Quality | +|---------------|------|-----------|----------------|---------------| +| `tests/uat/extension-page-harness.ts` (assertA33+; ROADMAP SC #1 + #2; A29 cs-injection-world rewrite) | test (page-side assertion host) | request-response + event-driven | `tests/uat/extension-page-harness.ts:3517-3700` (assertA30 cs-injection-world; Plan 03-02) for A33+; `tests/uat/extension-page-harness.ts:3363-3419` (existing assertA29; Plan 03-01) for the rewrite target | EXACT (same file; assertA30 is the canonical cs-injection-world precedent) | +| `tests/uat/lib/harness-page-driver.ts` (driveA33+ + driveA29 sentinel grep rewrite) | test (host-side driver + zip inspection + CDP `worker.close()`) | file-I/O (downloadsDir polling + JSZip-parse) + CDP (browser.waitForTarget + worker.close) | `tests/uat/lib/harness-page-driver.ts:2039-2148` (driveA30; Plan 03-02 — JSZip-parse + UserEvent grep); `tests/uat/lib/harness-page-driver.ts:1884-2001` (existing driveA29; Plan 03-01) for the rewrite target | EXACT for driveA33 sentinel grep; NEW PATTERN for `stopServiceWorker` CDP helper (cite RESEARCH §"Code Examples" Pattern 1) | +| `tests/uat/harness.test.ts` (orchestrator wiring for driveA33+; env-gating for 5-min lane) | test (orchestrator) | request-response (sequential driver dispatch) | `tests/uat/harness.test.ts:101-107` (Plan 03-01..04 import block); `tests/uat/harness.test.ts:344-357` (Plan 03-01..03 wrapped const block); `tests/uat/harness.test.ts:459-486` (Plan 03-01..04 drivers-array push block); `tests/uat/harness.test.ts:227-234` (existing `SKIP_PROD_REBUILD` env-gate pattern for the new `SKIP_LONG_UAT` gate) | EXACT (append-only across 3 sections; env-gate has internal precedent) | +| `tests/uat/extension-page-harness.ts` (A17.8 sub-check update for UI-SPEC inline-SVG) | test (assertion update) | DOM string-grep on bundled welcome chunk | self-analog: existing A17.8 at `tests/uat/extension-page-harness.ts:2249-2294` (Plan 01-10 + Wave 3 b112cb7) — flip data-URL grep to raw-SVG string grep | EXACT (1-block edit; new acceptance pattern documented in UI-SPEC §"Acceptance Criteria #3") | + +### D. Documentation edits (Phase 4 closure ceremony) + +| Modified/New File | Role | Data Flow | Closest Analog | Match Quality | +|-------------------|------|-----------|----------------|---------------| +| `.planning/ROADMAP.md` (D-P4-05 backfill Plans 01-08..01-13 rows) | docs (canonical ROADMAP) | docs synthesis | self-analog: existing Plans 01-01..01-07 row block at `.planning/ROADMAP.md:83-89` (canonical row shape) + Plans 01-08..01-13 already inline-tracked in row block at lines 90-95 — verify rows correctly enumerate per plan-checker flag #4 | EXACT (existing rows for 01-08..01-13 already present at lines 90-95; D-P4-05 task may just be a verification + addition of any missing row text per plan-checker re-audit) | +| `.planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md` (NEW; aggregator) | docs (verification aggregator with frontmatter) | docs synthesis | `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md` (4-override template; closest match for Plan 04-08 charter) + `.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md` (T5 override pattern) + `.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md` (per-requirement scorecard + cross-cutting gates + operator empirical acks + deferred items) | EXACT (Phase 3 03-VERIFICATION.md is the immediate-prior precedent; same override-block style; same gaps_closed / gaps_remaining shape) | +| `.planning/REQUIREMENTS.md` (no new REQs; ROADMAP success criteria verification status) | docs (requirements tracker) | docs synthesis | self-analog: existing REQ traceability table — Phase 4 adds NO new REQ rows per RESEARCH §"Phase Requirements" | NO-OP-CONTENT-CHANGE (frontmatter-only verification status update if needed) | +| `.planning/PROJECT.md` (Validated section evolves for v1 close) | docs (project tracker) | docs synthesis | self-analog: existing Validated section + DEC-* table; Phase 2/3 closure precedents at recent commits | EXACT (incremental Validated-line additions per Phase 4 plan landings) | +| `.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` (RESEARCH Finding 4 — flip stale "deferred to Phase 5" line) | docs (SUMMARY back-patch) | docs surgical edit | self-analog: existing closure note at `.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md:82` "Cursor-visibility refinement deferred to Phase 5..." — flip to "Shipped opportunistically in Plan 01-09 (`recorder.ts:285 cursor: 'always'`); verified in Phase 4 Plan 04-06" | EXACT (1-line back-patch; reference existing line 82 verbatim) | +| `.planning/phases/01-stabilize-video-pipeline/deferred-items.md` (RESEARCH Q1 — flip setimmediate entry) | docs (deferred-items tracker) | docs surgical edit | self-analog: existing entry "Plan 01-12 (Wave 7 pre-checkpoint bundle gates discovery)" at lines 7-42 — append closure note "Resolved in Phase 4 Plan 04-05 via `exclude: ['setimmediate']` + queueMicrotask inline polyfill" | EXACT (1-block append; reference existing 36-line entry verbatim) | + +## Pattern Assignments + +### `tests/build/no-new-function-in-sw-chunk.test.ts` (NEW; Wave 0; covers RESEARCH Q1 acceptance gate) + +**Analog:** `tests/build/no-remote-fonts.test.ts` (Plan 01-12 Wave 0 RED unit test) + +**Existing imports** (already in analog at lines 25-32): + +```typescript +import { execFile } from 'node:child_process'; +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { resolve as resolvePath } from 'node:path'; +import { promisify } from 'node:util'; + +import { describe, expect, it } from 'vitest'; + +const execFileAsync = promisify(execFile); +``` + +**Forbidden-strings inventory pattern** (analog lines 34-39): + +```typescript +const FORBIDDEN_REMOTE_STRINGS: ReadonlyArray = [ + 'fonts.googleapis.com', + 'https://fonts', + 'googleapis', +]; +``` + +Phase 4 adaptation for SW-chunk grep: + +```typescript +/** Forbidden patterns — any occurrence in dist/ SW chunk violates MV3 CSP hardening (Plan 04-05). */ +const FORBIDDEN_SW_CSP_PATTERNS: ReadonlyArray = [ + 'new Function', +]; +``` + +**Build + recursive walk pattern** (analog lines 41-66 + 90-100 + 102-108): + +```typescript +const PROD_BUILD_TIMEOUT_MS = 90_000; +const DIST_DIR = resolvePath(process.cwd(), 'dist'); + +function listAllFilesRecursive(root: string): ReadonlyArray { /* ... */ } +function countOccurrencesInFile(filePath: string, needle: string): number { /* ... */ } +function findMatchesInDist(needle: string): ReadonlyArray { /* ... */ } +async function runProductionBuild(): Promise { /* execFileAsync('npm', ['run', 'build'], ...) */ } +``` + +**Describe-block pattern** (analog lines 110-144): + +```typescript +describe('production bundle has no remote font URLs (MV3 CSP — T-01-12-01)', () => { + it('npm run build completes and dist/ exists', { timeout: PROD_BUILD_TIMEOUT_MS + 5_000 }, async () => { + if (process.env.SKIP_BUILD !== '1') { + await runProductionBuild(); + } + expect(existsSync(DIST_DIR), /* ... */).toBe(true); + }); + + for (const needle of FORBIDDEN_REMOTE_STRINGS) { + it(`dist/ does not contain '${needle}' (...)`, () => { + const matches = findMatchesInDist(needle); + expect(matches.length, /* failure diagnostic */).toBe(0); + }); + } +}); +``` + +Phase 4 adaptation: scope the file walk to the SW chunk only (`dist/assets/index.ts-*.js`) since cursor visibility / dead-code patterns may legitimately appear in offscreen/welcome chunks. Use `readdirSync(resolvePath(DIST_DIR, 'assets')).filter((n) => /^index\.ts-.*\.js$/.test(n))` to narrow. + +**Use for:** Plan 04-05 Wave 0 RED — single forbidden string `'new Function'`; expected count = 1 today (the setimmediate polyfill literal); expected count = 0 after the Plan 04-05 polyfill replacement lands. + +--- + +### `tests/build/dead-code-grep.test.ts` (NEW; Wave 0; covers RESEARCH SC #4) + +**Analog:** `tests/build/no-remote-fonts.test.ts` (same scaffold as above) + +**Phase 4 adaptation for src/ + vite.config.ts dead-code grep:** + +```typescript +const FORBIDDEN_DEAD_CODE: ReadonlyArray<{ readonly needle: string; readonly searchPaths: ReadonlyArray }> = [ + // Removed in Phase 1 Plan 01-05 SW shrink (REQ-manifest-permissions). + { needle: 'permissions.request', searchPaths: ['src'] }, + // Removed in Phase 1 Plan 01-06 build-pipeline collapse (DEC-006). + // Concrete sentinel is the literal HTML offscreen string the inline plugin emitted; + // see vite.config.ts pre-01-06 for the exact value. If the post-collapse audit + // can't pin a single string, drop the second entry and assert ONLY the first. + { needle: 'permissions.request', searchPaths: ['src', 'vite.config.ts'] }, +]; +``` + +**Use for:** Plan 04-05 Wave 0 RED. The expected count is 0 today (these were removed in Phase 1); the test pins regression. RESEARCH §"Pitfall 4" notes the dead-code grep is mostly checking work already done — the value is regression prevention. + +--- + +### `tests/build/cursor-visibility.test.ts` (OPTIONAL pin; covers RESEARCH Finding 4) + +**Analog:** `tests/build/no-remote-fonts.test.ts` (same scaffold, INVERTED polarity) + +**Phase 4 adaptation — positive single-file assertion:** + +```typescript +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve as resolvePath } from 'node:path'; + +const RECORDER_PATH = resolvePath(process.cwd(), 'src/offscreen/recorder.ts'); + +describe('cursor visibility constraint shipped (Plan 01-09 → verified Plan 04-06)', () => { + it("src/offscreen/recorder.ts contains `cursor: 'always'` in the getDisplayMedia constraints block", () => { + const text = readFileSync(RECORDER_PATH, 'utf8'); + expect(text).toContain("cursor: 'always'"); + }); +}); +``` + +**Use for:** Plan 04-06 Wave 0 (OPTIONAL — RESEARCH §"Wave 0 Gaps" calls it out as defense-against-accidental-deletion). Single-it pin; runs in <1s. If Plan 04-06 chooses minimum-surface, this test can be omitted; the grep gate then becomes a shell line in the SUMMARY. + +--- + +### `tests/content/fetch-interception.test.ts` (NEW dir + file; covers Audit P1 #11) + +**Analog:** `tests/background/start-video-capture-no-tab.test.ts` (Plan 01-09 RED unit test; pure-function-with-vi.fn-stub pattern) + +**Stub scaffold pattern** (analog lines 28-127): + +```typescript +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +interface BgChromeStub { /* ... declarative interface ... */ } +interface GlobalWithBgChrome { chrome?: BgChromeStub; indexedDB?: { /* ... */ }; } + +function buildBgStub(/* per-test args */): BgChromeStub { /* ... */ } +``` + +**Per-test driver pattern** (analog lines 135-155 — driveNotificationStart): + +```typescript +async function driveFetchInterception( + stub: BgChromeStub, + arg: string | Request, +): Promise<{ urlCaptured: string }> { + // 1. Install content-script fetch wrapper (import src/content/index.ts). + // 2. Invoke window.fetch with the test arg (string or Request). + // 3. Read the captured UserEvent.target (the URL field). + // 4. Return urlCaptured for assertion. +} +``` + +**3-test contract pattern** (analog lines 169-223 — Test A/B/C structure): + +```typescript +describe('Audit P1 #11 — fetch URL extraction handles Request + string args', () => { + beforeEach(() => { vi.resetModules(); }); + + it('A: fetch(stringUrl) captures the string URL', async () => { + // urlCaptured === 'https://example.com/api' (NOT '[object Request]') + }); + + it('B: fetch(new Request(url)) captures Request.url (NOT [object Request])', async () => { + // urlCaptured === request.url + }); + + it('C: fetch with response.ok=false records network_error with correct URL', async () => { + // REGRESSION GUARD: existing setupNetworkLogging flow continues to fire + }); +}); +``` + +**Use for:** Plan 04-02 Wave 0 — Test A/B are RED today, GREEN after the Plan 04-02 fix (`args[0]?.toString()` → `args[0] instanceof Request ? args[0].url : String(args[0])`). Test C is regression guard. + +**Key adaptation note:** The analog test imports `src/background/index.ts` after stubbing `chrome.*`. The Phase 4 test imports `src/content/index.ts` after stubbing `window.fetch` + a minimal `chrome.*` shim. The content script does NOT use chrome.runtime/chrome.tabs directly except for the `chrome.runtime.sendMessage` path; the stub can be much smaller than the analog's. + +--- + +### `tests/content/navigation-tracking.test.ts` (NEW; covers Audit P1 #14) + +**Analog:** Same as fetch-interception.test.ts above (Plan 01-09 stub scaffold pattern). + +**Phase 4-specific test pattern** (3-test contract): + +```typescript +describe('Audit P1 #14 — navigation URL tracking uses module-level previousUrl', () => { + beforeEach(() => { vi.resetModules(); }); + + it('A: popstate event captures previous URL (NOT "unknown")', async () => { + // 1. Load content/index.ts at https://example.com/start (initial location) + // 2. Update window.location.href to /next + // 3. Dispatch popstate + // 4. Assert UserEvent.meta.previousUrl === 'https://example.com/start' (NOT 'unknown') + }); + + it('B: hashchange captures previous URL', async () => { /* same shape */ }); + + it('C: history.pushState wraps + still emits previousUrl', async () => { /* regression */ }); +}); +``` + +**Use for:** Plan 04-02 Wave 0 — Test A is RED today (current code emits `'unknown'` via `history.state?.url || 'unknown'`); GREEN after module-level `previousUrl` tracking lands. + +--- + +### `tests/content/rrweb-timestamps.test.ts` (NEW; covers Audit P1 #15) + +**Analog:** Same scaffold (Plan 01-09 + Plan 01-10 module-import pattern). + +**Phase 4-specific test pattern** — assert timestamp is Unix-epoch-class (~1.7e12 in 2026), not small-int page-load-relative (<1e8): + +```typescript +describe('Audit P1 #15 — rrweb buffer cleanup uses Unix-epoch timestamps', () => { + beforeEach(() => { vi.resetModules(); }); + + it('A: rrweb event timestamps are Date.now()-class (>1e12), not page-load-relative (<1e8)', async () => { + // 1. Stub rrweb.record emit callback to capture event.timestamp values + // 2. Trigger an emit + // 3. Assert event.timestamp > 1e12 (Unix-epoch ms in 2026 ≈ 1.7e12) + }); + + it('B: cleanupOldEvents compares to Date.now() consistently', async () => { + // Regression: cleanupOldEvents at src/content/index.ts:27-50 already uses Date.now(); + // pin that the rrweb-side timestamp source matches so the (now - event.timestamp) + // arithmetic is meaningful (currently it's a category error). + }); +}); +``` + +**Use for:** Plan 04-02 Wave 0 — Test A is RED today (rrweb v2 alpha-pin emits relative timestamps); GREEN after wrapper normalizes via `Date.now()` at emit-callback time. + +--- + +### `tests/welcome/inline-svg.test.ts` (NEW dir + file; covers UI-SPEC inline-SVG injection) + +**Analog (primary):** `tests/background/onboarding.test.ts` (Plan 01-10 — welcome-page side-effect assertion pattern). +**Analog (secondary):** `tests/i18n/manifest-i18n.test.ts` (Plan 01-12 — file-read + string assertion shape). + +**Stub scaffold** (analog onboarding.test.ts lines 90-154): + +```typescript +function buildBgStub(): BgChromeStub { /* ... welcome page needs minimal chrome.i18n stub */ } + +async function drainMicrotasks(): Promise { + for (let i = 0; i < 16; i += 1) { + await Promise.resolve(); + } +} +``` + +**DOMParser injection assertion pattern** (3-test contract for the UI-SPEC implementation amendment): + +```typescript +describe('UI-SPEC dark-logo currentColor strategy — inline-SVG injection at populateMark()', () => { + beforeEach(() => { vi.resetModules(); }); + + it('A: populateMark() injects an inline element (NOT )', async () => { + // 1. Set up JSDOM-style document with welcome.html structure (or import the file) + // 2. Import src/welcome/welcome.ts + // 3. Trigger init()/DOMContentLoaded + // 4. Assert document.querySelector('.welcome-hero__mark svg') !== null + // 5. Assert document.querySelector('.welcome-hero__mark img') === null + }); + + it('B: injected SVG has stroke="currentColor" (canonical viewBox preserved)', async () => { + const svg = document.querySelector('svg.welcome-hero__mark-img'); + expect(svg?.getAttribute('stroke')).toBe('currentColor'); + expect(svg?.getAttribute('viewBox')).toBe('0 0 32 32'); + }); + + it('C: aria-hidden + class preserved through DOMParser round-trip', async () => { + const svg = document.querySelector('svg.welcome-hero__mark-img'); + expect(svg?.getAttribute('aria-hidden')).toBe('true'); + expect(svg?.classList.contains('welcome-hero__mark-img')).toBe(true); + }); +}); +``` + +**Use for:** Plan 04-06 Wave 0 — Tests A/B are RED today (current `populateMark()` injects `` per welcome.ts:165-172); GREEN after Plan 04-06 lands `?raw` import + DOMParser injection per UI-SPEC §"Implementation amendment". + +**Note on test placement:** new `tests/welcome/` directory creates a parallel to existing `tests/background/`, `tests/content/` (to be created above), `tests/i18n/`. This matches the source-tree mirror convention (src/welcome/ → tests/welcome/) used by Plan 01-09 + 01-10 + 01-12. + +--- + +### `src/content/index.ts:147` (Audit P1 #11 fetch URL extraction fix) + +**Analog:** self-analog at `src/content/index.ts:164-202` (existing `setupNetworkLogging` block; the wrap-pattern is canonical). + +**Existing wrapping pattern** (lines 164-172): + +```typescript +function setupNetworkLogging() { + // Перехват fetch + const originalFetch = window.fetch; + window.fetch = function(...args) { + return originalFetch.apply(this, args) + .then(response => { + if (!response.ok) { + // ... addUserEvent({type:'network_error', target: args[0]?.toString(), ...}) ← line 147 ref +``` + +**Phase 4 fix per RESEARCH §"Specifics":** + +```typescript +// BEFORE: +target: args[0]?.toString(), + +// AFTER (Plan 04-02 Audit P1 #11): +target: args[0] instanceof Request ? args[0].url : String(args[0]), +``` + +**Use for:** Plan 04-02 Task A — single-line type-narrow. Pinned by `tests/content/fetch-interception.test.ts` Tests A/B. + +--- + +### `src/content/index.ts:99-109` (Audit P1 #14 navigation URL tracking fix) + +**Analog:** self-analog at `src/content/index.ts:21-24` (existing module-level `let rrwebEvents` / `let userEvents` pattern — additive same shape). + +**Existing navigation handler** (lines 99-109): + +```typescript +function setupNavigationLogging() { + const handleNavigation = () => { + addUserEvent({ + timestamp: Date.now(), + type: 'navigation', + target: 'window', + value: window.location.href, + url: window.location.href, + meta: { previousUrl: history.state?.url || 'unknown' }, // ← BUG: always 'unknown' in apps that don't populate history.state + }); + }; + // ... +} +``` + +**Phase 4 fix per RESEARCH §"Specifics":** + +```typescript +// AT module scope (around line 25, alongside existing `let userEvents`): +let previousUrl = window.location.href; + +// IN handleNavigation: +const handleNavigation = () => { + const fromUrl = previousUrl; + const toUrl = window.location.href; + previousUrl = toUrl; + addUserEvent({ + timestamp: Date.now(), + type: 'navigation', + target: 'window', + value: toUrl, + url: toUrl, + meta: { previousUrl: fromUrl }, + }); +}; +``` + +**Use for:** Plan 04-02 Task B — module-level state addition + handler rewrite. Pinned by `tests/content/navigation-tracking.test.ts` Tests A/B/C. + +--- + +### `src/content/index.ts:36-42` (Audit P1 #15 rrweb timestamps semantics fix) + +**Analog:** self-analog at `src/content/index.ts:53-54` (existing `addUserEvent` epoch convention). + +**Existing UserEvent epoch convention** (lines 52-58): + +```typescript +function addUserEvent(event: UserEvent) { + event.timestamp = Date.now(); // ← canonical Unix epoch for the UserEvent log + event.url = window.location.href; + userEvents.push(event); + // ... +} +``` + +**Phase 4 fix per RESEARCH §"Specifics":** mirror this `Date.now()` epoch convention into the rrweb-emit-callback wrapper. Where rrweb's emit callback pushes to `rrwebEvents` and the event carries a relative timestamp, normalize by overriding `event.timestamp = Date.now()` at emit time. + +```typescript +// In the rrweb wiring (file location: search for `rrweb.record({ emit:` or +// equivalent; canonical location per RESEARCH is src/content/index.ts where +// rrwebEvents is populated): +record({ + emit: (event) => { + event.timestamp = Date.now(); // Plan 04-02 Audit P1 #15: normalize to Unix epoch for events.json downstream + rrwebEvents.push(event); + }, +}); +``` + +**Use for:** Plan 04-02 Task C — additive normalization line. Pinned by `tests/content/rrweb-timestamps.test.ts` Tests A/B. + +--- + +### `src/shared/brand/mokosh-mark.svg` (UI-SPEC stroke recolor) + +**Analog:** self-analog at line 2 (single-line attribute change on root `` element). + +**Change** (per UI-SPEC §"Implementation contract" + amendment): + +```svg + + + + + +``` + +Only `stroke="#181b2a"` → `stroke="currentColor"`. The 12 `` + 1 `` children inherit `stroke` from the root; do NOT touch them. + +**Use for:** Plan 04-06 (UI-SPEC dark-logo strategy). 1-character edit. The 12+1 children at lines 4, 7-9, 11-13, 17-19, 22-24 stay byte-identical. + +--- + +### `src/welcome/welcome.ts:46 + 159-179` (UI-SPEC inline-SVG injection) + +**Analog:** self-analog at `src/welcome/welcome.ts:159-179` (existing `populateMark()` with `` injection). + +**Existing `populateMark` shape (lines 159-179):** + +```typescript +function populateMark(): void { + const slots = Array.from( + document.querySelectorAll('[data-mokosh-slot="mark"]'), + ); + const altText = COPY['welcome.hero.mark.alt'] ?? 'Знак Mokosh'; + for (const slot of slots) { + const img = document.createElement('img'); + img.src = markUrl; + img.alt = altText; + img.width = 64; + img.height = 64; + img.className = 'welcome-hero__mark-img'; + img.setAttribute('aria-hidden', 'true'); + slot.replaceChildren(img); + } + if (slots.length === 0) { + logger.warn('populateMark: no [data-mokosh-slot="mark"] element found in DOM'); + } +} +``` + +**Phase 4 rewrite per UI-SPEC §"Implementation amendment":** + +```typescript +// AT line 46: +// BEFORE: +import markUrl from '../shared/brand/mokosh-mark.svg?url'; +// AFTER: +import markSvg from '../shared/brand/mokosh-mark.svg?raw'; + +// AT populateMark (filter-pipeline form preserved per project rule): +function populateMark(): void { + const slots = Array.from( + document.querySelectorAll('[data-mokosh-slot="mark"]'), + ); + const parser = new DOMParser(); + const altText = COPY['welcome.hero.mark.alt'] ?? 'Знак Mokosh'; + for (const slot of slots) { + const doc = parser.parseFromString(markSvg, 'image/svg+xml'); + const svg = doc.documentElement; + svg.classList.add('welcome-hero__mark-img'); + svg.setAttribute('aria-hidden', 'true'); + svg.setAttribute('role', 'img'); + svg.setAttribute('aria-label', altText); + slot.replaceChildren(svg); + } + if (slots.length === 0) { + logger.warn('populateMark: no [data-mokosh-slot="mark"] element found in DOM'); + } +} +``` + +**MV3 CSP discipline (per UI-SPEC §"Executor"):** DOMParser + appendChild only. NEVER `innerHTML`. The DOMParser parse is safe because the input is a Vite-bundled compile-time literal (no runtime untrusted input). + +**Use for:** Plan 04-06 main implementation. Pinned by `tests/welcome/inline-svg.test.ts` Tests A/B/C. + +--- + +### `src/welcome/welcome.css:91-95` (UI-SPEC selector update) + +**Analog:** self-analog at lines 91-95 (existing `.welcome-hero__mark-img` rule). + +**Existing rule** (lines 91-95): + +```css +.welcome-hero__mark-img { + width: 60%; + height: 60%; + display: block; +} +``` + +**Phase 4 — minimum surgical change:** the existing class selector matches both `` (current) and `` (post-Plan-04-06). No selector edit needed. The width/height % values render correctly on inline SVG as well (the SVG's viewBox handles the aspect ratio). + +UI-SPEC contemplates an optional explicit-selector transition: `img.welcome-hero__mark-img, svg.welcome-hero__mark-img { ... }`. This is OPTIONAL because the bare class selector already matches both. Planner picks based on diff-minimality preference. + +**Color inheritance verification:** `.welcome-hero__mark` at lines 64-73 sets `color: var(--mks-fg-inverse)`. Inline `` children inherit `color` from parent (per W3C SVG2 §13.3 "Specifying paint"). `stroke="currentColor"` resolves to `var(--mks-fg-inverse)` = `--mks-linen-50` on the wrapper's madder-orange BG → contrast PRESERVED. + +**Use for:** Plan 04-06 — verification line only OR optional 1-rule selector broadening. Either is acceptable. + +--- + +### `globals.d.ts` (UI-SPEC `*.svg?raw` ambient module decl) + +**Analog:** self-analog at lines 34-37 (existing `*.svg?url` ambient module decl). + +**Existing ambient decl** (lines 34-37): + +```typescript +declare module '*.svg?url' { + const url: string; + export default url; +} +``` + +**Phase 4 addition (append after line 37):** + +```typescript +// Plan 04-06 UI-SPEC dark-logo `currentColor` strategy: ambient module +// declaration for Vite `?raw` asset imports. The `?raw` suffix returns +// the asset source as a string at build time (mirrors Vite's `?url` API +// but yields the file contents instead of a hashed URL). Used by +// src/welcome/welcome.ts populateMark() to inline-inject the canonical +// mark SVG into the DOM so the `stroke="currentColor"` resolves via the +// wrapper's CSS `color` cascade. +// +// References: +// - Vite raw imports: https://vite.dev/guide/assets.html#importing-asset-as-string +declare module '*.svg?raw' { + const raw: string; + export default raw; +} +``` + +**Use for:** Plan 04-06 — additive 4-line block. Mirrors existing convention; tsc clean. + +--- + +### `src/background/index.ts` top-of-module setimmediate polyfill prelude (RESEARCH Q1) + +**Analog:** self-analog at lines 1-17 (existing imports block; the new prelude inserts before `import { Logger }`). + +**Existing imports** (lines 1-17): + +```typescript +import { Logger } from '../shared/logger'; +import { base64ToBlob, blobToBase64 } from '../shared/binary'; +// ... long import block +``` + +**Phase 4 prelude per RESEARCH §"Code Examples" Pattern 2:** + +```typescript +// Plan 04-05 CSP hardening — replace vite-plugin-node-polyfills' setimmediate +// polyfill (which includes a CSP-unsafe `new Function(string)` fallback for +// string-form setImmediate calls that this codebase never uses). JSZip falls +// back to its inline polyfill chain (MessageChannel / postMessage / setTimeout) +// when globalThis.setImmediate is unset; we provide the safest fast-path +// explicitly. Reversible by `git revert`. +// +// Reference: RESEARCH §"Code Examples" Pattern 2; Q1 finding. +if (typeof globalThis.setImmediate === 'undefined') { + (globalThis as { setImmediate?: (fn: (...args: unknown[]) => void, ...args: unknown[]) => void }).setImmediate = + (fn, ...args) => queueMicrotask(() => fn(...args)); +} + +import { Logger } from '../shared/logger'; +// ... rest of existing imports unchanged +``` + +**TypeScript note:** the assignment uses a typed widening cast (no `as any`) per project rule. The function signature matches Node's `setImmediate` shape sufficiently for JSZip's call sites (which always pass `fn` as a function, not a string per RESEARCH Finding Q1). + +**Use for:** Plan 04-05 — 8-line additive prelude. Pinned by `tests/build/no-new-function-in-sw-chunk.test.ts` (post-fix `new Function` count = 0 in SW chunk). + +--- + +### `vite.config.ts` (RESEARCH Q1 `exclude: ['setimmediate']` config) + +**Analog:** self-analog at lines 14-22 (existing `nodePolyfills({...})` config). + +**Existing config** (lines 14-22): + +```typescript +nodePolyfills({ + include: ['buffer'], + globals: { + Buffer: true, + global: false, + process: false, + }, + protocolImports: false, +}), +``` + +**Phase 4 addition per RESEARCH Q1 Option (a):** + +```typescript +nodePolyfills({ + include: ['buffer'], + exclude: ['setimmediate'], // Plan 04-05 CSP hardening — drops `new Function` from SW chunk + globals: { + Buffer: true, + global: false, + process: false, + }, + protocolImports: false, +}), +``` + +**TypeScript note:** the plugin's TypeScript types support `exclude: string[]` per RESEARCH Q1 Finding (verified via WebSearch 2026-05-21). No type-error expected; pinned by `npx tsc --noEmit`. + +**Use for:** Plan 04-05 — 1-line addition. Coheres with the SW-entry prelude above. + +--- + +### `generate-icons.js` → `generate-icons.cjs` (RESEARCH SC #3 ESM/CJS fix) + +**Analog:** self-analog at line 1 (`const fs = require('fs')`) — the literal CJS smoking-gun. + +**Phase 4 fix per CONTEXT §"Specifics":** + +Rename the file: `git mv generate-icons.js generate-icons.cjs`. No code change inside the file. Node treats `.cjs` files as CommonJS regardless of the enclosing `"type": "module"` in package.json (per Node docs https://nodejs.org/api/packages.html#determining-module-system). This is the surgical-minimum fix; the alternative (rewrite to ESM with `import fs from 'node:fs'`) is also acceptable but a wider diff. + +**Acceptance gate:** + +```bash +$ node generate-icons.cjs # exit 0 +$ npm run build # exit 0 (unchanged; build doesn't invoke generate-icons) +``` + +**Use for:** Plan 04-05 — single file rename. Update any reference in package.json scripts (if present) + README/CLAUDE.md docs (if any reference). Search: `rg 'generate-icons\.js' .` before the rename to enumerate references. + +--- + +### `tests/uat/extension-page-harness.ts` (assertA33+ for ROADMAP SC #1 + #2; assertA29 rewrite per RESEARCH Q3) + +**Analog (assertA33+):** `tests/uat/extension-page-harness.ts:3517-3700` (assertA30 — canonical cs-injection-world; Plan 03-02). + +**Analog (assertA29 rewrite):** `tests/uat/extension-page-harness.ts:3363-3419` (existing assertA29 — Plan 03-01) + `tests/uat/extension-page-harness.ts:3517-3700` (assertA30 cs-injection-world target). + +**Constants-block pattern** (analog assertA30 at lines 3487-3501): + +```typescript +const A30_SAVE_ARCHIVE_TIMEOUT_MS = 15_000; +const A30_SEGMENT_SETTLE_MS = 11_000; +const A30_TRIGGER_SETTLE_MS = 1_000; +const A30_TAB_NAVIGATION_WAIT_MS = 1_500; +const A30_PROBE_TAB_URL = 'https://example.com/'; +const A30_404_PROBE_URL = 'https://example.com/this-path-does-not-exist-404-probe-a30'; +``` + +Phase 4 adaptation for A33 (5-min SW idle test) per RESEARCH §"Code Examples" Pattern 4: + +```typescript +const A33_IDLE_WAIT_MS = 5 * 60 * 1000; // 300_000 — real wall-clock +const A33_NEW_SW_BOOT_MS = 500; // post-worker.close() settle +const A33_OVERALL_TIMEOUT_MS = A33_IDLE_WAIT_MS + 60_000; // 360_000 +const A33_SAVE_ARCHIVE_TIMEOUT_MS = 15_000; +``` + +Phase 4 adaptation for A29 rewrite per RESEARCH §"Code Examples" Pattern 3: + +```typescript +const A29_PROBE_TAB_URL = 'https://example.com/'; +const A29_TAB_NAVIGATION_WAIT_MS = 1_500; +const A29_SEGMENT_SETTLE_MS = 11_000; +const A29_MUTATION_SETTLE_MS = 500; +const A29_SAVE_ARCHIVE_TIMEOUT_MS = 15_000; +const A29_MUTATION_SENTINEL = 'a29-mutation-sentinel'; +const A29_PROBE_DIV_ID = 'a29-probe-mutation'; +``` + +**cs-injection-world assertion body pattern** (analog assertA30 lines 3517-3636 verbatim shape): + +```typescript +async function assertAXX(): Promise { + const result: AssertionResult = { + passed: false, + name: 'AXX — (SPEC §10 #X)', + checks: [], + diagnostics: [], + }; + + let probeTabId: number | undefined; + + try { + diag(result, 'Step 1: setupFreshRecording'); + const setupResp = await setupFreshRecording(); + if (!setupResp.ok) { + throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`); + } + + diag(result, `Step 2: chrome.tabs.create(${AXX_PROBE_TAB_URL}, active:true)`); + const probeTab = await chrome.tabs.create({ url: AXX_PROBE_TAB_URL, active: true }); + probeTabId = probeTab.id; + if (probeTabId === undefined) { + throw new Error('chrome.tabs.create returned undefined tab.id'); + } + + diag(result, `Step 3: wait ${AXX_TAB_NAVIGATION_WAIT_MS}ms for navigation + content script attach`); + await new Promise((r) => setTimeout(r, AXX_TAB_NAVIGATION_WAIT_MS)); + + diag(result, `Step 4: settle ${AXX_SEGMENT_SETTLE_MS}ms for first segment rotation`); + await new Promise((r) => setTimeout(r, AXX_SEGMENT_SETTLE_MS)); + + diag(result, 'Step 5: chrome.scripting.executeScript ISOLATED — inject triggers'); + await chrome.scripting.executeScript({ + target: { tabId: probeTabId }, + world: 'ISOLATED', + func: (sentinel: string, divId: string) => { + // per-AXX injection body + }, + args: [/* per-AXX args */], + }); + + diag(result, `Step 6: settle ${AXX_TRIGGER_SETTLE_MS}ms`); + await new Promise((r) => setTimeout(r, AXX_TRIGGER_SETTLE_MS)); + + diag(result, 'Step 7: dispatch SAVE_ARCHIVE (probe tab is active)'); + const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>( + { type: 'SAVE_ARCHIVE' }, + AXX_SAVE_ARCHIVE_TIMEOUT_MS, + 'SAVE_ARCHIVE (AXX)', + ); + + result.checks.push({ + name: 'AXX.1: SAVE_ARCHIVE ack received with success=true', + expected: true, + actual: ack.success, + passed: ack.success === true, + }); + + result.passed = result.checks.every((c) => c.passed); + } catch (err) { + result.error = err instanceof Error ? err.message : String(err); + diag(result, `THREW: ${result.error}`); + } + // NOTE: assertA30 omits chrome.tabs.remove in finally; the probe tab + // is left active for the SAVE flow. Phase 4 assertA33+ may cleanup or + // leave per the plan author's choice. + + return result; +} +``` + +**Use for:** +- Plan 04-01 — rewrite assertA29 using this skeleton + the rrweb mutation injector per RESEARCH §"Code Examples" Pattern 3 +- Plan 04-03 — assertA33 5-min idle (Step 4 swap: replace 11s segment-settle with 300s idle wait; no probe tab needed — host-side `worker.close()` does the SW kill) +- Plan 04-04 — extend assertA30 OR new assertA34 for ROADMAP SC #2 fetch+XHR network_error empirical validation (the 404 fetch is already in assertA30 — Plan 04-04 may simply validate the existing host-side check is binding, OR add an XHR variant) + +**`__mokoshHarness` registration lockstep** (analog at lines 3332-3404 — Plan 02-04 + extended by Plan 03-01..04): + +For each new `assertA` added: +1. Add type signature to `declare global { interface Window { __mokoshHarness: { ... assertA: () => Promise; ... } } }`. +2. Add entry to `window.__mokoshHarness = { ... assertA, ... };` object literal. + +Missing either side breaks the orchestrator's `harness.assertA()` call. + +--- + +### `tests/uat/lib/harness-page-driver.ts` (driveA33+ + driveA29 sentinel grep rewrite + `stopServiceWorker` CDP helper) + +**Analog (driveA33 sentinel grep):** `tests/uat/lib/harness-page-driver.ts:2039-2148` (driveA30 — JSZip-parse + UserEvent grep; Plan 03-02). + +**Analog (driveA29 rewrite):** `tests/uat/lib/harness-page-driver.ts:1884-2001` (existing driveA29; Plan 03-01) — replace EventType-loose-grep with strict sentinel grep per RESEARCH Q3. + +**NEW PATTERN — `stopServiceWorker` CDP helper** (cite RESEARCH §"Code Examples" Pattern 1; no codebase analog): + +```typescript +import type { Browser } from 'puppeteer'; + +/** + * Force-terminate the MV3 service worker via Puppeteer CDP. Required + * because Puppeteer's persistent CDP attach keeps SWs alive indefinitely; + * natural 30s idle eviction does NOT fire under test conditions per Chrome + * docs (https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle). + * + * Reference: https://developer.chrome.com/docs/extensions/how-to/test/test-serviceworker-termination-with-puppeteer + * + * @param browser - Puppeteer Browser handle from harness setup. + * @param extensionId - The runtime extension ID (from handles.extensionId). + */ +async function stopServiceWorker(browser: Browser, extensionId: string): Promise { + const host = `chrome-extension://${extensionId}`; + const target = await browser.waitForTarget( + (t) => t.type() === 'service_worker' && t.url().startsWith(host), + ); + const worker = await target.worker(); + if (worker !== null) { + await worker.close(); + } +} +``` + +**driveA33 host-side body pattern** (per RESEARCH §"Code Examples" Pattern 4): + +```typescript +export async function driveA33( + page: Page, + browser: Browser, + extensionId: string, + downloadsDir: string, +): Promise { + const r: AssertionRecord = { name: 'A33', passed: false, checks: [], diagnostics: [] }; + + // Step 1: prime recording via the page-side harness call + const pageResult = await page.evaluate(async () => { + const harness = (window as any).__mokoshHarness; + return await harness.setupFreshRecordingForA33(); + }); + + // Step 2: 5-min wall-clock idle + r.diagnostics.push(`waiting ${A33_IDLE_WAIT_MS}ms for SW idle window`); + await new Promise((res) => setTimeout(res, A33_IDLE_WAIT_MS)); + + // Step 3: force SW termination via CDP + await stopServiceWorker(browser, extensionId); + r.diagnostics.push('SW terminated via worker.close()'); + + // Step 4: brief settle for SW teardown + await new Promise((res) => setTimeout(res, A33_NEW_SW_BOOT_MS)); + + // Step 5: dispatch SAVE_ARCHIVE — wakes SW back up as an event + const saveResult = await page.evaluate(() => { + const harness = (window as any).__mokoshHarness; + return harness.dispatchSaveArchiveForA33(); + }); + r.checks.push({ + name: 'A33.1: SAVE_ARCHIVE ack success after 5-min idle + SW kill', + expected: true, + actual: saveResult.success, + passed: saveResult.success === true, + }); + + // Step 6: verify zip contains non-empty video buffer + const zipPath = findLatestZip(downloadsDir); + if (zipPath === null) { + r.checks.push({ name: 'A33.2: zip present', expected: '>=1 zip', actual: 'none', passed: false }); + r.passed = false; + return r; + } + const zip = await JSZip.loadAsync(readFileSync(zipPath)); + const videoEntry = zip.file('video/last_30sec.webm'); + const videoSize = videoEntry !== null + ? (await videoEntry.async('uint8array')).byteLength + : 0; + r.checks.push({ + name: 'A33.2: video/last_30sec.webm size > 0 (buffer survived SW eviction)', + expected: '>0', + actual: String(videoSize), + passed: videoSize > 0, + }); + r.checks.push({ + name: 'A33.3: video size > 100 KB (sanity floor; real archives 1-3 MB)', + expected: '>100000', + actual: String(videoSize), + passed: videoSize > 100_000, + }); + + r.passed = r.checks.every((c) => c.passed); + return r; +} +``` + +**driveA29 strict-sentinel rewrite pattern** (per RESEARCH §"Code Examples" Pattern 3 host-side block): + +```typescript +import { EventType, IncrementalSource } from '@rrweb/types'; + +const A29_MUTATION_SENTINEL = 'a29-mutation-sentinel'; + +export async function driveA29( + page: Page, + downloadsDir: string, +): Promise { + // Phase 1: page-side stub call (existing pattern) + const pageResult = await page.evaluate(async () => { + const harness = (window as any).__mokoshHarness; + return await harness.assertA29() as AssertionRecord; + }); + + const mergedChecks: CheckRecord[] = pageResult.checks.slice(); + const mergedDiagnostics: string[] = pageResult.diagnostics.slice(); + + // Phase 2: locate the zip + const zipPath = findLatestZip(downloadsDir); + if (zipPath === null) { + mergedChecks.push({ + name: 'A29.0: zip present in downloadsDir', + expected: '>=1 zip', + actual: 'none', + passed: false, + }); + return { passed: false, name: pageResult.name, checks: mergedChecks, diagnostics: mergedDiagnostics }; + } + + // Phase 3: strict sentinel grep (replaces existing loose EventType count check) + const zip = await JSZip.loadAsync(readFileSync(zipPath)); + const sessionRaw = await zip.file('rrweb/session.json')?.async('text') ?? '[]'; + const events = JSON.parse(sessionRaw) as Array<{ type: number; data?: any }>; + + const mutationEvents = events.filter((e) => + e.type === EventType.IncrementalSnapshot && + e.data?.source === IncrementalSource.Mutation, + ); + const sentinelEvents = mutationEvents.filter((e) => { + const adds = e.data?.adds ?? []; + return adds.some((a: any) => + typeof a?.node?.textContent === 'string' && + a.node.textContent.includes(A29_MUTATION_SENTINEL), + ); + }); + mergedChecks.push({ + name: `A29.2: rrweb captured the injected mutation containing '${A29_MUTATION_SENTINEL}' (closes iana.org-leftover-flake gap)`, + expected: `>=1 mutation`, + actual: String(sentinelEvents.length), + passed: sentinelEvents.length >= 1, + }); + + const mergedPassed = mergedChecks.every((c) => c.passed); + return { passed: mergedPassed, name: pageResult.name, checks: mergedChecks, diagnostics: mergedDiagnostics }; +} +``` + +**Use for:** +- Plan 04-01 — driveA29 strict-sentinel rewrite (closes A29 flake per RESEARCH Q3) +- Plan 04-03 — driveA33 5-min idle (new helper + new driver; needs `Browser` + `extensionId` propagated through `driveA33Wrapped`) +- Plan 04-04 — driveA34 (or extend driveA30 — TBD by planner) for the ROADMAP SC #2 fetch+XHR network_error empirical sanity check + +**Filter-pipeline form** (per project rule `~/.claude/CLAUDE.md` Control Flow): no `continue` statements; use `.filter()` chains as shown. + +--- + +### `tests/uat/harness.test.ts` (orchestrator wiring for driveA33+; env-gating for 5-min lane) + +**Analog (import + wrapping + push):** `tests/uat/harness.test.ts:101-107 + 344-357 + 459-486` (Plan 03-01..04 — append-only across 3 sections). + +**Analog (env-gating):** `tests/uat/harness.test.ts:227-234` (existing `SKIP_PROD_REBUILD` pattern in the A0 grep gate). + +**Import block** (analog lines 100-107): + +```typescript +// Plan 03-01 — rrweb DOM verification (SPEC §10 #4 / REQ-rrweb-dom-buffer) +driveA29, +// Plan 03-02 — event-log verification (SPEC §10 #5 / REQ-user-event-log) +driveA30, +// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02) +driveA31, +// Plan 03-04 — RAM scaffolding best-effort (SPEC §10 #9 per D-P3-04) +driveA32, +``` + +Phase 4 addition: + +```typescript +// Plan 04-03 — SW state persistence 5-min idle (ROADMAP SC #1) +driveA33, +// Plan 04-04 — fetch + XHR network_error empirical (ROADMAP SC #2) +driveA34, // OR no new driver if Plan 04-04 extends driveA30 in-place +``` + +**Wrapped-driver block** (analog lines 344-357): + +```typescript +// Plan 03-01 — driveA29 needs downloadsDir for host-side JSZip parse +const driveA29Wrapped: (page: import('puppeteer').Page) => Promise = + (page) => driveA29(page, handles.downloadsDir); +// ... A30, A31 ... +``` + +Phase 4 addition for A33 (NEW SHAPE — needs `Browser` + `extensionId` beyond the standard `Page`): + +```typescript +// Plan 04-03 — driveA33 needs Browser + extensionId for CDP-based SW kill +// AND downloadsDir for host-side JSZip parse of post-restart zip. +const driveA33Wrapped: (page: import('puppeteer').Page) => Promise = + (page) => driveA33(page, handles.browser, handles.extensionId, handles.downloadsDir); +``` + +**Verify `handles` includes `browser` + `extensionId`:** check the existing `handles` object shape (search for `interface HarnessHandles` or equivalent). If `browser` and `extensionId` aren't already exposed, Plan 04-03 needs to extend `handles` — which is a small additive change with no overlap concerns. + +**Env-gating pattern** (analog lines 227-234): + +```typescript +if (process.env.SKIP_PROD_REBUILD !== '1') { + process.stdout.write('A0: running `npm run build` (set SKIP_PROD_REBUILD=1 to skip)...\n'); + // ... await execFileAsync('npm', ['run', 'build'], ...); +} else { + process.stdout.write('A0: SKIP_PROD_REBUILD=1 — using existing dist/\n'); +} +``` + +Phase 4 adaptation for `SKIP_LONG_UAT` (per RESEARCH §"Q2 sub-question (c)" recommendation): + +```typescript +{ + name: 'A33', + drive: process.env.SKIP_LONG_UAT === '1' + ? async (): Promise => ({ + name: 'A33', + passed: true, + checks: [], + diagnostics: ['A33 SKIPPED (SKIP_LONG_UAT=1; unset to run 5-min idle test)'], + }) + : driveA33Wrapped, +}, +``` + +**Default-skip recommendation:** the env-gate defaults to `SKIP_LONG_UAT=undefined` (test runs) for the Phase 4 closure + alpha-distribution gate. For per-commit developer iteration, document `SKIP_LONG_UAT=1` in the SUMMARY. Per RESEARCH Open Question 2, the planner picks the polarity. + +**Drivers-array push pattern with banner** (analog lines 459-486): + +```typescript +// Plan 03-01 A29: rrweb DOM verification (SPEC §10 #4). +{ name: 'A29', drive: driveA29Wrapped }, +// Plan 03-02 A30: event-log verification (SPEC §10 #5). +{ name: 'A30', drive: driveA30Wrapped }, +// Plan 03-03 A31: password-filter PARTIAL. +{ name: 'A31', drive: driveA31Wrapped }, +// Plan 03-04 A32: RAM scaffolding. +{ name: 'A32', drive: driveA32 }, +``` + +Phase 4 addition (banner cites RESEARCH section + plan number): + +```typescript +// Plan 04-03 A33: SW state persistence 5-min idle (ROADMAP SC #1; RESEARCH Q2). +// Forces SW eviction via Puppeteer CDP worker.close() per the canonical +// Chrome devrel pattern (RESEARCH Pattern 1). Verifies offscreen-RAM +// segments survive SW restart. Env-gated by SKIP_LONG_UAT for fast +// per-commit iteration; defaults to RUN for Phase 4 closure + alpha gate. +{ name: 'A33', drive: /* env-gated wrapper above */ }, +``` + +**FORBIDDEN_HOOK_STRINGS stays at 12** per CONTEXT §"Claude's Discretion" + RESEARCH §"Anti-Patterns": A33 rides production CDP surfaces (`browser.waitForTarget` + `worker.close()`) — NO new `__MOKOSH_UAT__`-gated test symbols. If Plan 04-03 needs a new `setupFreshRecordingForA33` page-side helper, that helper can be a thin wrapper around the existing `setupFreshRecording` (no new bridge ops; no new define-token cells). + +**Lockstep requirement:** ANY new `__MOKOSH_UAT__`-gated symbol introduced MUST be added to BOTH this UAT A0 mirror (orchestrator file) AND the unit-gate at `tests/background/no-test-hooks-in-prod-bundle.test.ts:108-126`. Phase 4 expects no such addition; plan-checker confirms the inventory stays at 12. + +--- + +### `tests/uat/extension-page-harness.ts` A17.8 sub-check update (UI-SPEC inline-SVG) + +**Analog:** self-analog at `tests/uat/extension-page-harness.ts:2294` (existing A17.8 — Plan 01-10 + Wave 3 b112cb7 mark-bundling invariant). + +**Existing assertion (line 2294 + surrounding block 2249-2310 region):** + +```typescript +{ + name: 'A17.8: welcome chunk JS bundles the canonical mark SVG (data URL OR file URL) AND canonical viewBox preserved (Plan 01-10 must_have #9 path-A swap-in)', + // ... checks `data:image/svg+xml,...` OR `` presence + viewBox='0 0 32 32' in welcome chunk +} +``` + +**Phase 4 update per UI-SPEC §"Acceptance Criteria #3":** + +A17.8 splits OR rewrites to assert: +- **A17.8a — raw SVG source bundled:** the welcome chunk JS contains the raw SVG source as a string literal (the `?raw` import inlines it). Grep for the canonical mark's signature characters (e.g., `'viewBox="0 0 32 32"'` + `'stroke="currentColor"'`). +- **A17.8b — inline SVG injected at populateMark() runtime:** after the welcome page loads, `document.querySelector('.welcome-hero__mark svg')` is non-null AND its `stroke` attribute resolves via `currentColor` (parent CSS color cascade). + +**Use for:** Plan 04-06 — coordinated with the implementation change. The harness assertion is the canonical post-edit gate; pin the inline-SVG transition through both unit test + harness coverage. + +--- + +### `.planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md` (NEW; aggregator) + +**Analog (primary):** `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md` (Phase 3 closure; same charter shape — verification of ROADMAP success criteria + overrides). + +**Analog (secondary):** `.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md` (T5 override template; 5/5 must-haves) + `.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md` (per-requirement scorecard + cross-cutting gates + operator empirical acks + deferred items). + +**Frontmatter shape (per Phase 3 03-VERIFICATION.md lines 1-79):** + +```yaml +--- +phase: 04-harden-clean-up-optional +verified: 2026-05-21TZ +status: passed +score: 4/4 ROADMAP success criteria + N/N P1 polish items + M/M flake fixes +overrides_applied: +re_verification: + previous_status: + previous_score: + gaps_closed: + - "ROADMAP SC #1 — SW state persistence A33 GREEN with offscreen-RAM-survives-SW verified" + - "ROADMAP SC #2 — fetch + XHR network_error empirical via A30 (already covered) + A34 XHR variant if Plan 04-04 ships" + - "ROADMAP SC #3 — generate-icons.cjs rename verified" + - "ROADMAP SC #4 — dead-code grep tests/build/dead-code-grep.test.ts GREEN" + - "Audit P1 #11/#14/#15 — content-script tests GREEN" + - "setimmediate polyfill — dist/ SW chunk grep 0 hits for 'new Function'" + - "A29 flake — 5/5 PASS across consecutive runs after strict-sentinel rewrite" + - "Cursor visibility — verified shipped at recorder.ts:285 (RESEARCH Finding 4); 01-07-SUMMARY.md back-patched" + - "Dark-logo currentColor strategy — inline-SVG injection verified via tests/welcome/inline-svg.test.ts + A17.8 update" + - "ROADMAP backfill Plans 01-08..01-13 — verified per plan-checker re-audit" +override_notes: + - dimension: "" + initial_status: "<...>" + override_to: "<...>" + rationale: | + +human_verification: + - test: "" + expected: "<...>" + why_human: "<...>" +deferred: + - truth: "rrweb 2.0.0-alpha.4 → stable v2 upgrade" + addressed_in: "v1.1 / v2 maintenance milestone" + evidence: "D-P4-01 charter exclusion + alpha-pin stability across 13 plans + 34/34 UAT GREEN" + - truth: "Programmatic SW-realm RAM measurement" + addressed_in: "v1.1 / v2 maintenance milestone" + evidence: "D-P3-04 + D-P4-01 charter exclusion" + - truth: "REQ-password-confidentiality v2 candidate" + addressed_in: "v1.1 / v2 maintenance milestone IF charter reverses" + evidence: "D-P3-02 + D-P4-01 charter exclusion 2026-05-20" +--- +``` + +**Body sections per Phase 1 01-VERIFICATION.md** (lines 33-104): + +- **Per-Requirement Scorecard** — Phase 4 has NO new REQs (per RESEARCH §"Phase Requirements") but verifies the ROADMAP SCs row-by-row with evidence citations +- **Cross-Cutting Gates** — update UAT harness row (33→34 or 34→35 drivers post Plan 04-03 + 04-04); preserve baseline vitest + Tier-1 + bundle gates; cite Plan 04-08's pre-checkpoint bundle gate re-run per saved memory `feedback-pre-checkpoint-bundle-gates.md` +- **Operator-Empirical Acks (verbatim + commit refs)** — append the Phase 4 ack (dark-mode operator visual check on welcome hero per UI-SPEC #6 — the ONE Phase 4 operator-empirical checkpoint per UI-SPEC) +- **Deferred Items** — the 3 v2-deferral items above + +**Score format note:** Phase 3 used `score: 5/5 ROADMAP ... (9/9 SPEC §10 criteria — 8 automated + 1 best-effort scaffolding with operator/alpha fallback)`. Phase 4 follows the same compound shape since Phase 4 has both ROADMAP SC + non-ROADMAP polish items. + +**Use for:** Plan 04-08 (closure plan). Frontmatter overrides + scorecard + operator ack lines are filled in at close time based on Phase 4 execution outcomes. + +--- + +### `.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` back-patch (RESEARCH Finding 4) + +**Analog:** self-analog at line 82 (existing closure note). + +**Existing line 82:** + +```markdown +- **Cursor-visibility refinement deferred to Phase 5** — added to the P1/P2 hardening list with the explicit user-observation citation (2026-05-15). +``` + +**Phase 4 surgical back-patch:** + +```markdown +- **Cursor-visibility refinement opportunistically shipped in Plan 01-09** — `src/offscreen/recorder.ts:285` carries `cursor: 'always'` per the inline comment "Plan 01-09 D-15-display-surface ... opportunistically lifts the Phase 5 cursor-visibility refinement". Verified in Phase 4 Plan 04-06 via grep gate `tests/build/cursor-visibility.test.ts` + operator-empirical SAVE flow showing pointer visible in `last_30sec.webm`. This SUMMARY line replaces the prior "deferred to Phase 5" framing (stale post-Plan-01-09 opportunism). +``` + +Plan 04-06 also flips line 47, 82, 109, 135, 205 references to "Phase 5" → "Phase 4 verification" OR removes the stale framing where appropriate. + +**Use for:** Plan 04-06 docs hygiene task. 1-block edit. The 5 stale "Phase 5" references in 01-07-SUMMARY.md collectively become a corrected narrative. + +--- + +### `.planning/phases/01-stabilize-video-pipeline/deferred-items.md` flip (RESEARCH Q1) + +**Analog:** self-analog at lines 7-42 (existing entry "Plan 01-12 (Wave 7 pre-checkpoint bundle gates discovery)"). + +**Existing entry shape (lines 36-42):** + +```markdown +- **Suggested follow-up:** Switch from `vite-plugin-node-polyfills`'s + full `Buffer` polyfill to a tree-shake-friendly minimal Buffer + shim — or audit downstream deps for direct `Buffer.*` usage and + inline the few needed primitives. Either approach drops the + setimmediate polyfill entirely. +``` + +**Phase 4 append (after line 42):** + +```markdown +- **Resolved in Phase 4 Plan 04-05** (commit ``): kept `vite-plugin-node-polyfills` (Buffer is still legitimately needed by JSZip via the upstream `buffer` package) but added `exclude: ['setimmediate']` to the plugin config + inserted a 4-line `queueMicrotask`-based `setImmediate` polyfill at the top of `src/background/index.ts`. JSZip's inline polyfill chain handles the standard cases; the explicit fast-path matches the W3C `queueMicrotask` semantics. Post-fix: `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0 (was 1). Acceptance: pinned by `tests/build/no-new-function-in-sw-chunk.test.ts` Wave 0 RED. +``` + +**Use for:** Plan 04-05 docs ceremony at landing. 1-block append; references existing entry verbatim. + +--- + +## Shared Patterns + +### Approach B Harness Extension Lockstep (3-file rule) + +**Source:** Plan 02-04 (Phase 2 closure) + Plan 01-13 (Approach B foundation) + Plan 03-01..04 (Phase 3 closure precedent) +**Apply to:** Every Phase 4 plan that ships a new harness assertion (Plan 04-01 A29 rewrite, Plan 04-03 A33, Plan 04-04 A34 [if separate]) + +3-file lockstep per new (or rewritten) `assertA` + `driveA`: + +1. **`tests/uat/extension-page-harness.ts`** — append `async function assertA(): Promise` (page-side stub OR orchestrator) + extend `declare global { interface Window { __mokoshHarness: { ... assertA: ... } } }` interface + add entry to `window.__mokoshHarness = { ... }` object literal. +2. **`tests/uat/lib/harness-page-driver.ts`** — append `export async function driveA(page: Page, ...): Promise` (3-phase pattern: page.evaluate stub → findLatestZip → JSZip read; OR a CDP-based variant for A33). +3. **`tests/uat/harness.test.ts`** — append `driveA` import + wrapped-driver const (if `downloadsDir` / `browser` / `extensionId` are needed) + drivers-array push with banner comment. + +Missing ANY one of the three breaks the orchestrator (either at the `harness.assertA()` page.evaluate call OR at the `for (const drive of drivers)` loop). Plan-checker MUST validate all three are touched per new A-number. + +### Pre-Checkpoint Bundle Gates (6/6 Standard Inventory) + +**Source:** Saved memory `feedback-pre-checkpoint-bundle-gates.md` + `02-04-SUMMARY.md` (Phase 2 closure precedent) + Plan 03-05 pre-checkpoint sweep +**Apply to:** Plan 04-08 (closure plan; before VERIFICATION.md is written) — ALL plans before any operator step + +Standard 6/6 inventory (Plan 02-04 ran 7; Plan 03-05 ran 6 per the prior phase's PATTERNS): + +| Gate | Concrete Check | +|------|----------------| +| Gate 1 | `npm run build` exit 0 | +| Gate 2 | SW CSP-safety: `grep -rn "new Function\\|eval(" dist/assets/` — **AFTER Plan 04-05: expect 0 hits** (was 1 documented exception for setimmediate polyfill) | +| Gate 3 | SW Node-globals: `grep -rn "Buffer.from\\|Buffer.alloc\\|require(" dist/assets/index.ts-*.js` — 0 hits | +| Gate 4 | DOM-globals: `grep -rn "window.\\|document." dist/assets/index.ts-*.js` — bundled-lib idiom (typeof-guarded) | +| Gate 5 | Tier-1 SW-bundle-import gate (`tests/background/sw-bundle-import.test.ts`) GREEN | +| Gate 6 | FORBIDDEN_HOOK_STRINGS unit gate (`tests/background/no-test-hooks-in-prod-bundle.test.ts`) — 12 strings, 0 hits each | + +**Plan 04-05 specifically flips Gate 2 polarity** (1 → 0). Pre-checkpoint sweep must verify the flip landed. + +### T5 Override Pattern (Harness-Coverage-as-Verification) + +**Source:** `02-VERIFICATION.md:1-31` + saved memory `feedback-trust-harness-over-manual-uat.md` +**Apply to:** Plan 04-08 04-VERIFICATION.md — for SCs where harness coverage exists AND was a candidate for operator empirical + +When verifier returns `human_needed` for a criterion AND a harness assertion empirically covers the same surface: +- Move `human_verification` entry to `overrides_applied` block in frontmatter +- Include explicit user-delegation citation (date + verbatim quote, if recorded) + saved-memory reference +- Sub-bullet each harness assertion that covers the surface +- Mark `status: passed` rather than `status: human_needed` + +**Counter-example (do NOT override):** dark-mode operator visual check on welcome hero per UI-SPEC #6 — genuine non-automatable (operator-perceptible aesthetic judgment of contrast on a dark surface). This is the canonical `human_verification` entry for Phase 4. + +### Filter-Pipeline Form (No `continue`) + +**Source:** `~/.claude/CLAUDE.md` "Control Flow" § + driveA28 (lines 1808-1810) + welcome.ts populateCopy/populateI18n/populateMark (existing canonical examples) +**Apply to:** All Phase 4 source edits + test file enumeration loops + +```typescript +// PREFERRED — filter pipeline (existing welcome.ts populateCopy at lines 62-71): +const pairs = els + .map((el) => ({ el, key: el.getAttribute('data-mokosh-key') })) + .filter((p): p is { el: HTMLElement; key: string } => typeof p.key === 'string') + .map((p) => ({ ...p, value: COPY[p.key] })) + .filter((p): p is { el: HTMLElement; key: string; value: string } => typeof p.value === 'string'); + +// AVOID — for...of + continue +``` + +### Production-Surface-Only New Assertions (FORBIDDEN_HOOK_STRINGS stays at 12) + +**Source:** CONTEXT §"Claude's Discretion" + RESEARCH §"Anti-Patterns" + Phase 3 PATTERNS lockstep +**Apply to:** Plans 04-01 (A29 rewrite uses production cs-injection), Plan 04-03 (A33 uses production CDP `worker.close()`), Plan 04-04 (A34 uses production fetch/XHR) + +Phase 4 harness extensions ride production surfaces. NO new `__MOKOSH_UAT__`-gated test-only symbols expected. Plan-checker MUST validate inventory count stays at 12 across: +- `tests/uat/harness.test.ts:123-138` (UAT A0 mirror) +- `tests/background/no-test-hooks-in-prod-bundle.test.ts:108-126` (unit gate) + +If any new bridge op IS needed (e.g., Plan 04-03 needs a `setupFreshRecordingForA33` page-side helper that's NOT a thin wrapper), the inventory grows by 1-2 entries and BOTH mirrors must update. + +### TypeScript Discipline (No `as any`) + +**Source:** `~/.claude/CLAUDE.md` TypeScript § ("Type arrow function parameters explicitly") + Plan 01-14 + Plan 01-12 precedents +**Apply to:** Plan 04-05 (setimmediate polyfill type cast) + Plan 04-06 (DOMParser injection) + +Per existing code (recorder.ts:288-291 DisplayMediaStreamOptions typed widening): use typed casts via inline interface intersection, NOT `as any`. Example for the polyfill cast: + +```typescript +(globalThis as { setImmediate?: (fn: (...args: unknown[]) => void, ...args: unknown[]) => void }).setImmediate = + (fn, ...args) => queueMicrotask(() => fn(...args)); +``` + +### Anti-Pattern: Re-reading the Same Range + +**Source:** Operator guidance for agents +**Apply to:** All Phase 4 implementation work — read each source file ONCE, extract patterns, then implement + +--- + +## No Analog Found (or NEW PATTERN required) + +| File | Role | Data Flow | Reason / Mitigation | +|------|------|-----------|---------------------| +| `stopServiceWorker` helper (inline in `tests/uat/lib/harness-page-driver.ts` per Plan 04-03 minimum-surface; OR new `tests/uat/lib/sw-control.ts` if planner prefers separation) | test (host-side CDP helper) | CDP (browser.waitForTarget + worker.close) | NEW PATTERN per RESEARCH §"Code Examples" Pattern 1. No codebase analog; the Puppeteer CDP `worker.close()` surface has never been used in this codebase prior to Phase 4. Cite RESEARCH §"Code Examples" Pattern 1 + the Chrome devrel doc verbatim. Recommend INLINE in `harness-page-driver.ts` per scope-minimization (analogous to RESEARCH §"Code Examples" Pattern 4 driveA33 wiring). | + +All other anticipated files have direct analogs from Phase 1/2/3 plans. + +## Per-Plan Anticipated File Touch Inventory + +Per CONTEXT §"Decisions" §"Claude's Discretion" suggested grouping (planner may consolidate; 6-7 plans target): + +| Plan | Files Modified | Files Created | Pattern Source | +|------|----------------|---------------|----------------| +| 04-01 (bug/flake stabilization) | `tests/uat/extension-page-harness.ts` (assertA29 rewrite) + `tests/uat/lib/harness-page-driver.ts` (driveA29 strict-sentinel) + `tests/uat/harness.test.ts` (no change unless wrapping changes) | — | RESEARCH §"Code Examples" Pattern 3 + Plan 03-02 assertA30/driveA30 | +| 04-02 (audit P1 polish #11+#14+#15) | `src/content/index.ts` (3 surgical edits at lines 36-42, 99-109, 147) | `tests/content/fetch-interception.test.ts` + `tests/content/navigation-tracking.test.ts` + `tests/content/rrweb-timestamps.test.ts` (3 new test files + new tests/content/ dir) | Plan 01-09 start-video-capture-no-tab.test.ts stub scaffold + RESEARCH §"Specifics" diff snippets | +| 04-03 (SW state persistence ROADMAP SC #1) | `tests/uat/extension-page-harness.ts` (assertA33 + harness method) + `tests/uat/lib/harness-page-driver.ts` (driveA33 + stopServiceWorker helper) + `tests/uat/harness.test.ts` (import + wrap + push + SKIP_LONG_UAT env-gate) | — | RESEARCH §"Code Examples" Pattern 4 (driveA33) + Pattern 1 (stopServiceWorker); spike-first per RESEARCH §"Q2 sub-question (b)" recommendation | +| 04-04 (ROADMAP SC #2 fetch+XHR network_error) | `tests/uat/lib/harness-page-driver.ts` (extend driveA30 OR new driveA34) + `tests/uat/harness.test.ts` (push if A34 separate) | possibly none if driveA30 extends in-place | Plan 03-02 assertA30/driveA30 — fetch path already exists; XHR variant additive | +| 04-05 (build/CSP hygiene + dead-code + generate-icons + setimmediate) | `vite.config.ts` (exclude config) + `src/background/index.ts` (top-of-module polyfill prelude) + `.planning/phases/01-stabilize-video-pipeline/deferred-items.md` (closure flip) + `generate-icons.js` → `generate-icons.cjs` (rename) | `tests/build/no-new-function-in-sw-chunk.test.ts` + `tests/build/dead-code-grep.test.ts` (Wave 0 RED tests) | `tests/build/no-remote-fonts.test.ts` (Plan 01-12) + RESEARCH §"Code Examples" Pattern 2 | +| 04-06 (visual polish: cursor verification + dark-logo) | `src/shared/brand/mokosh-mark.svg` (1-char stroke recolor) + `src/welcome/welcome.ts` (?url→?raw + populateMark rewrite) + `src/welcome/welcome.css` (optional selector broadening) + `globals.d.ts` (?raw ambient decl) + `tests/uat/extension-page-harness.ts` (A17.8 sub-check update) + `.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` (back-patch lines 47, 82, 109, 135, 205) | `tests/welcome/inline-svg.test.ts` (Wave 0 RED) + OPTIONAL `tests/build/cursor-visibility.test.ts` (defensive pin) | UI-SPEC §"Implementation amendment" + RESEARCH Finding 4 + Plan 01-10 welcome page existing populateMark pattern | +| 04-07 (A31 extension — one-line rrweb session.json sentinel grep; may fold into 04-02 per CONTEXT) | `tests/uat/extension-page-harness.ts` (assertA31 1-line extension) + `tests/uat/lib/harness-page-driver.ts` (driveA31 sentinel grep) | — | Plan 03-03 assertA31/driveA31 existing pattern | +| 04-08 (VERIFICATION + ROADMAP backfill + alpha re-distribution + v1 close) | `.planning/ROADMAP.md` (Plans 01-08..01-13 row verification per D-P4-05) + `.planning/REQUIREMENTS.md` (no new REQs; status update if needed) + `.planning/PROJECT.md` (Validated section evolves for v1 close) | `.planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md` | Phase 3 03-VERIFICATION.md + Phase 2 02-VERIFICATION.md + Phase 1 01-VERIFICATION.md combined patterns | + +## Wave Sequencing Note + +Per CONTEXT §"Claude's Discretion" + Phase 2/3 lessons: Plans modifying the SAME 3 harness files (`extension-page-harness.ts`, `harness-page-driver.ts`, `harness.test.ts`) need sequential wave assignments to avoid `files_modified` overlap. Plans 04-01, 04-03, 04-04, 04-07 all touch these files. + +Suggested wave-decomposition: + +- **Wave 1 (parallel-safe)**: Plan 04-02 (src/content/ + new tests/content/ — disjoint from harness files), Plan 04-05 (build config + new tests/build/ — disjoint from harness files) +- **Wave 2 (sequential within wave)**: Plan 04-01 (A29 rewrite) → Plan 04-03 (A33 5-min idle) → Plan 04-04 (A34 if separate) → Plan 04-07 (A31 extension; may fold into Plan 04-02 instead per CONTEXT) +- **Wave 3 (parallel-safe)**: Plan 04-06 (welcome page + brand asset + A17.8 update — touches `extension-page-harness.ts` but in disjoint line range from Wave 2; planner should verify A17.8 update region doesn't conflict with Wave 2 A33+ append region — likely safe since A17.8 is line 2294 and A33+ is line ~3800+; plan-checker validates) +- **Wave 4 (closure)**: Plan 04-08 — VERIFICATION.md + alpha re-distribution + v1 close prep (sequential after all above) + +Per CONTEXT line 184: "plan-checker should catch `files_modified` overlap within a wave; if detected, decompose into sequential waves." + +## Metadata + +**Analog search scope:** `tests/uat/`, `tests/background/`, `tests/build/`, `tests/i18n/`, `.planning/phases/01-stabilize-video-pipeline/`, `.planning/phases/02-stabilize-export-pipeline/`, `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/`, `src/content/`, `src/welcome/`, `src/shared/brand/`, `src/background/`, `vite.config.ts`, `globals.d.ts`, `generate-icons.js`, `.planning/ROADMAP.md` + +**Files read (each ONCE, non-overlapping ranges):** +- `.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md` (full; 242 lines) +- `.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md` (3 non-overlapping ranges: 1-400, 400-800, 800-929 — 929 lines total) +- `.planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md` (full; 347 lines) +- `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md` (full; structural reference) +- `tests/build/no-remote-fonts.test.ts` (full; 145 lines) +- `tests/background/start-video-capture-no-tab.test.ts` (full; 225 lines) +- `tests/background/onboarding.test.ts` (full; 270 lines) +- `tests/i18n/manifest-i18n.test.ts` (full; 131 lines) +- `tests/background/no-test-hooks-in-prod-bundle.test.ts:95-185` (FORBIDDEN_HOOK_STRINGS unit gate) +- `src/content/index.ts:1-170` (P1 #11/#14/#15 target sites) +- `src/welcome/welcome.ts` (full; 199 lines — populateMark target) +- `src/welcome/welcome.css:60-110` (welcome-hero__mark + welcome-hero__mark-img rules) +- `src/shared/brand/mokosh-mark.svg` (full; 25 lines) +- `globals.d.ts` (full; 38 lines) +- `src/offscreen/recorder.ts:255-295` (cursor: 'always' verification site) +- `src/background/index.ts:1-80` (top-of-module imports — polyfill prelude insertion point) +- `vite.config.ts` (full; 75 lines) +- `tests/uat/extension-page-harness.ts:3343-3520` (existing A29 + start of A30 cs-injection-world) +- `tests/uat/harness.test.ts:95-145, 340-490, 215-240` (3 non-overlapping ranges: imports + wrappers + push + env-gate) +- `.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md:75-100` (cursor visibility deferred-to-Phase-5 stale lines) +- `.planning/phases/01-stabilize-video-pipeline/deferred-items.md` (full; 42 lines — setimmediate entry) +- `.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md:1-80` (T5 override frontmatter + Observable Truths table) +- `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md:1-80` (Phase 3 closure frontmatter) +- `.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md:1-80` (scorecard + cross-cutting gates + operator acks) +- `.planning/ROADMAP.md:83-100, 35-40` (Plan-row block + Phase-row block) + +**Pattern extraction date:** 2026-05-21