Files
mokosh/.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
Mark 3c1280ed2d docs(04): plan-phase closure — 3 cosmetic advisories from checker iter-2 resolved
Plan-checker iter-2 returned VERIFICATION PASSED with 3 cosmetic advisories:
- Dim 11: RESEARCH.md "## Open Questions" missing "(RESOLVED)" suffix → fixed
- Dim 12: PATTERNS.md:886 stale dispatchSaveArchiveForA33 example → added
  DEPRECATED banner citing Plan 04-04 REVISION iter-2 Option B canonical pattern
- VALIDATION.md frontmatter "4 revised tasks" mismatched per-task map (5 rows) → fixed

All 4 BLOCKER+WARNING issues from iter-1 verified resolved by iter-2 plan-checker
(VERIFICATION PASSED). 3 cosmetic items now resolved as well. 2 advisory items
left as-is per iter-1 (W2 scope-sanity at 04-06; W3 conservative 04-03 dep).

Phase 4 plans cleared for execution:
- 7 plans across 6 waves (Wave 1: 04-01+04-02 parallel; Waves 2-6 single-plan)
- Plan-checker iter-2 VERIFICATION PASSED
- Test baselines preserved: vitest 171/171 · UAT harness 33/33 · Tier-1 12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:21:03 +02:00

75 KiB

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.jsgenerate-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):

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):

const FORBIDDEN_REMOTE_STRINGS: ReadonlyArray<string> = [
  'fonts.googleapis.com',
  'https://fonts',
  'googleapis',
];

Phase 4 adaptation for SW-chunk grep:

/** 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):

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):

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:

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:

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):

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):

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):

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):

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):

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):

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):

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):

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":

// 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):

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":

// 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):

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.

// 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):

<!-- 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):

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":

// 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):

.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):

declare module '*.svg?url' {
  const url: string;
  export default url;
}

Phase 4 addition (append after line 37):

// 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):

import { Logger } from '../shared/logger';
import { base64ToBlob, blobToBase64 } from '../shared/binary';
// ... long import block

Phase 4 prelude per RESEARCH §"Code Examples" Pattern 2:

// 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):

nodePolyfills({
  include: ['buffer'],
  globals: {
    Buffer: true,
    global: false,
    process: false,
  },
  protocolImports: false,
}),

Phase 4 addition per RESEARCH Q1 Option (a):

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.jsgenerate-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:

$ 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):

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:

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:

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):

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):

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):

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):

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):

// 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:

// 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):

// 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):

// 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):

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):

{
  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):

// 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):

// 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):

{
  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):

---
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:

- **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:

- **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):

- **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):

- **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

// 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:

(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.jsgenerate-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