# 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