# 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 `<svg>` 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 `<svg>` 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 `<img>` with parsed `<svg>` 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<string> = [
  '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<string> = [
  '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<string> { /* ... */ }
function countOccurrencesInFile(filePath: string, needle: string): number { /* ... */ }
function findMatchesInDist(needle: string): ReadonlyArray<ForbiddenMatch> { /* ... */ }
async function runProductionBuild(): Promise<void> { /* 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<string> }> = [
  // 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<void> {
  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 <svg> element (NOT <img>)', 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 `<img>` 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 `<svg>` element).

**Change** (per UI-SPEC §"Implementation contract" + amendment):

```svg
<!-- BEFORE (line 2): -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#181b2a" stroke-width="2.25" stroke-linecap="square" stroke-linejoin="miter">

<!-- AFTER: -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="square" stroke-linejoin="miter">
```

Only `stroke="#181b2a"` → `stroke="currentColor"`. The 12 `<line>` + 1 `<rect>` 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 `<img>` injection).

**Existing `populateMark` shape (lines 159-179):**

```typescript
function populateMark(): void {
  const slots = Array.from(
    document.querySelectorAll<HTMLElement>('[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<HTMLElement>('[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 `<img class="welcome-hero__mark-img">` (current) and `<svg class="welcome-hero__mark-img">` (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 `<svg>` 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<AssertionResult> {
  const result: AssertionResult = {
    passed: false,
    name: 'AXX — <purpose> (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<NN>` added:
1. Add type signature to `declare global { interface Window { __mokoshHarness: { ... assertA<NN>: () => Promise<AssertionResult>; ... } } }`.
2. Add entry to `window.__mokoshHarness = { ... assertA<NN>, ... };` object literal.

Missing either side breaks the orchestrator's `harness.assertA<NN>()` 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<void> {
  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<AssertionRecord> {
  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
  // ⚠ DEPRECATED iter-2 (2026-05-21) — `__mokoshHarness.dispatchSaveArchiveForA33` does NOT exist
  //   on the harness surface (verified: grep extension-page-harness.ts:4018 confirms
  //   __mokoshHarness.{assertA1..A31, getManifestVersion} only). Per Plan 04-04 REVISION
  //   iter-2 (Option B), dispatch SAVE_ARCHIVE inline via:
  //     const saveResult = await page.evaluate(() => new Promise((resolve) => {
  //       chrome.runtime.sendMessage({ type: 'SAVE_ARCHIVE' }, (ack) => resolve(ack));
  //     }));
  //   Matches 9 existing assertA* methods (A5/A11/A12/A13/A26/A28/A29/A30/A31).
  //   See 04-04-PLAN.md <interfaces> for the canonical pattern.
  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<AssertionRecord> {
  // 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<AssertionRecord> =
  (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<AssertionRecord> =
  (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<AssertionRecord> => ({
        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 `<asset hash url>` 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-21T<HH-MM-SS>Z
status: passed
score: 4/4 ROADMAP success criteria + N/N P1 polish items + M/M flake fixes
overrides_applied: <count>
re_verification:
  previous_status: <if applicable; v1 close>
  previous_score: <if applicable>
  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: "<per-criterion override if any T-style harness-coverage cases land>"
    initial_status: "<...>"
    override_to: "<...>"
    rationale: |
      <verbatim memory-citation block>
human_verification:
  - test: "<if any genuine non-automatable case lands; e.g. dark-mode operator visual check per UI-SPEC #6>"
    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 `<HASH>`): 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<NN>` + `driveA<NN>`:

1. **`tests/uat/extension-page-harness.ts`** — append `async function assertA<NN>(): Promise<AssertionResult>` (page-side stub OR orchestrator) + extend `declare global { interface Window { __mokoshHarness: { ... assertA<NN>: ... } } }` interface + add entry to `window.__mokoshHarness = { ... }` object literal.
2. **`tests/uat/lib/harness-page-driver.ts`** — append `export async function driveA<NN>(page: Page, ...): Promise<AssertionRecord>` (3-phase pattern: page.evaluate stub → findLatestZip → JSZip read; OR a CDP-based variant for A33).
3. **`tests/uat/harness.test.ts`** — append `driveA<NN>` 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<NN>()` 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
