Files
mokosh/.planning/phases/04-harden-clean-up-optional/04-02-SUMMARY.md
Mark 6a1fc32826 docs(04-02): complete harden-clean-up-optional plan 04-02 — build hygiene
Plan 04-02 closes three independent build-hygiene fixes consolidated into
one plan because they share the build-gate-grep test-scaffold pattern:

1. **setimmediate polyfill replacement** — layered 4-mechanism CSP-hardening
   eliminates the `new Function` literal from the SW chunk (grep -c flips
   1→0 across all three SW chunks). Runtime guard + nodePolyfills exclude
   + resolve.alias + Rollup post-transform plugin. Option α (force JSZip
   unbundled lib/index.js) attempted + reverted because it broke
   readable-stream-browser propagation causing UAT A30+ regressions;
   Option β (post-transform plugin) preserves JSZip's pre-bundled
   distribution verbatim while excising the offending literal.

2. **ROADMAP SC #3** (generate-icons ESM/CJS) — `git mv generate-icons.js
   generate-icons.cjs` resolves the `require('fs')` under
   `package.json type: module` via Node's `.cjs`-as-CJS rule.

3. **ROADMAP SC #4** (dead-code grep) — `tests/build/dead-code-grep.test.ts`
   regression-pins `permissions.request` absence in `src/`.

Plus closure of Plan 01-12 Wave 7's setimmediate deferred-items entry.

Task commits:
  - 630d40c test(04-02): Wave 0 RED — no-new-function + dead-code-grep
  - f251297 feat(04-02): Wave 1 GREEN — setimmediate replacement + CJS rename + closure

Verification:
  - vitest 180/180 → 183/183 GREEN on clean run (+3 net new tests)
  - UAT harness 33/33 GREEN preserved (REVISION iter-2 WARNING 1 empirical pin)
  - Pre-checkpoint bundle gates 5/5 PASS; SW CSP-safety polarity flipped 1→0
  - tsc-clean preserved; npm run build exit 0; node generate-icons.cjs exit 0

STATE.md: Plan 3/7 (Plan 04-02 complete); 25/30 total plans; 83% progress.
ROADMAP.md: Phase 4 progress 2/7 plans complete (04-01 + 04-02).
deferred-items.md: Plan 01-12 Wave 7 setimmediate entry CLOSED end-to-end.

SUMMARY at `.planning/phases/04-harden-clean-up-optional/04-02-SUMMARY.md`.

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

213 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 04-harden-clean-up-optional
plan: 02
subsystem: build-hygiene
tags: [setimmediate-polyfill, csp-hardening, generate-icons-cjs, dead-code-grep, roadmap-sc-3, roadmap-sc-4, tdd, charter-d-p4-01, rollup-plugin]
# Dependency graph
requires:
- phase: 01-stabilize-video-pipeline
provides: tests/build/no-remote-fonts.test.ts scaffold (Plan 01-12 — Wave 0 RED unit test scaffold mirrored verbatim for the SW-chunk CSP grep) + the Plan 01-12 Wave 7 deferred-items.md disclosure entry (closure target)
- phase: 04-harden-clean-up-optional
provides: Plan 04-01 baseline (vitest 180/180 GREEN on the clean run; 04-01 closed at HEAD f72bca5)
provides:
- SW chunk `new Function` literal eliminated end-to-end via 4-layered CSP-hardening mitigation (runtime prelude + plugin exclude + resolve.alias + Rollup post-transform)
- tests/build/no-new-function-in-sw-chunk.test.ts — SW-chunk CSP grep build-gate (RED→GREEN flip in this plan; future regression pin)
- tests/build/dead-code-grep.test.ts — ROADMAP SC #4 regression pin against `permissions.request` re-introduction in src/
- src/shared/setimmediate-stub.ts — minimal queueMicrotask-based polyfill, the resolve.alias.setimmediate target
- generate-icons.cjs — ESM/CJS disambiguation under package.json type:module (ROADMAP SC #3 GREEN)
- .planning/phases/01-stabilize-video-pipeline/deferred-items.md closure-flip (Plan 01-12 Wave 7 setimmediate entry now CLOSED)
- 2 new build-gate vitest tests + 1 new it() in the build-prep gate = +3 net new tests (180 → 183 GREEN on clean run)
affects: [04-03 (flake stabilization; deferred-items.md format precedent + the established Option β post-transform plugin pattern available for similar future excisions), 04-07 (Phase 4 closure aggregator — this plan closes ROADMAP SC #3 + SC #4 + the setimmediate hardening side-quest)]
# Tech tracking
tech-stack:
added: [] # No new runtime/dev dependencies; pure config + source changes against existing stack
patterns:
- "Layered-mitigation pattern for transitive-bundled CSP-unsafe code: runtime guard (prelude installs safe global BEFORE consumer evaluates) + bundler exclude (eliminates plugin-injected redundant polyfill) + resolve.alias (catches bare-specifier requires) + Rollup post-transform (excises text literals from pre-bundled distributions that bypass the resolver). Established at vite.config.ts for setimmediate; reusable for any future `new Function`/`eval` literal that escapes plugin-level filtering."
- "Build-gate vitest pattern (continued from Plan 01-12 tests/build/no-remote-fonts.test.ts): SKIP_BUILD=1 escape hatch + recursive walk + countOccurrencesInFile + describe-block-per-needle. Phase 4 narrowed scope: glob-filter `^index\\.ts-.*\\.js$` for the SW chunk only (BLOCKER 1 fix from plan-checker iter-1: the earlier `index*-bg.js` pattern matched nothing)."
- "Pre-bundled-dependency interception strategy: when a node_module ships a pre-bundled distribution (browser-field-mapped) that contains its own internal module registry, Vite's resolve.alias cannot reach inside; the Rollup `generateBundle` post-transform hook is the canonical interception point (executes against final chunk text, after all other plugins)."
key-files:
created:
- "tests/build/no-new-function-in-sw-chunk.test.ts"
- "tests/build/dead-code-grep.test.ts"
- "src/shared/setimmediate-stub.ts"
modified:
- "vite.config.ts (4 edits: node:url import + Plugin type import; nodePolyfills.exclude:['setimmediate']; resolve.alias.setimmediate → src/shared/setimmediate-stub.ts; stripSetimmediateNewFunction() Rollup post-transform plugin definition + registration in plugins array)"
- "src/background/index.ts (17-line top-of-module prelude inserted BEFORE the first import; queueMicrotask-based setimmediate polyfill with typed widening cast)"
- ".planning/phases/01-stabilize-video-pipeline/deferred-items.md (closure-flip block appended at EOF; Plan 01-12 Wave 7 entry now marked Resolved in Phase 4 Plan 04-02 with full 4-mechanism mitigation documentation)"
renamed:
- "generate-icons.js → generate-icons.cjs (history preserved via git mv; no code change)"
key-decisions:
- "Adopted Option β (Rollup `generateBundle` post-transform plugin that text-replaces `(I=new Function(\"\"+I))``(I=function(){})` in any chunk containing the JSZip-bundled setimmediate IIFE) AFTER empirically discovering that Option α (force JSZip's unbundled lib/index.js entry via resolve.alias.jszip) broke UAT harness A30+ assertions. Option α reverted in the same Task 2 work session before commit; the failure mode was JSZip's async-write pipeline (readable-stream-browser) not transitively wiring correctly through Vite's resolver when forced off the browser-field-mapped pre-bundled distribution."
- "Layered the mitigation across four mechanisms (runtime prelude + plugin exclude + resolve.alias + post-transform) rather than relying on any single one. The runtime prelude alone makes the bundle CSP-safe AT RUNTIME (JSZip's setimmediate IIFE's `if(!s.setImmediate){...}` guard skips the offending body once globalThis.setImmediate is installed), but the static `new Function` literal would still be present in the bundle text — failing the build-gate test AND remaining a static-analysis red flag for future audits. The post-transform plugin closes that gap surgically."
- "Used `fileURLToPath(new URL('./...', import.meta.url))` for the resolve.alias target path instead of a bare `'/src/...'` prefix or `path.resolve(__dirname, ...)` — the leading-slash form is interpreted as filesystem root by Vite's resolver (would fail in non-root cwds), while `__dirname` is undefined under vite.config.ts's ESM mode. The `import.meta.url` form is the canonical ESM idiom per Vite docs."
- "Documented the Option α attempt + reversion verbatim in the Task 2 commit body so future Phase 4+ executors investigating similar transitive-polyfill issues understand WHY the unbundled-entry approach fails for JSZip specifically (browser-field readable-stream-browser dep chain breakage)."
patterns-established:
- "Layered transitive-polyfill CSP-hardening: runtime guard + bundler exclude + bundler alias + Rollup post-transform — applied to setimmediate, generalizable to any future polyfill of this shape (string-coercion fallback in unreachable IIFE branch)."
- "When a node_module ships a pre-bundled browser-field distribution that contains its own internal module registry (CJS-style numbered slot table), Vite's resolve.alias cannot intercept the internal requires. The Rollup `generateBundle` hook is the canonical post-processing interception point — runs against final chunk text after all bundler plugins have completed; safe for surgical literal replacement when the upstream IIFE is unreachable at runtime."
- "Build-gate test glob convention for SW-chunk-only assertions: `dist/assets/index.ts-*.js` (matches both the SW entry chunk and the loader chunk; excludes welcome/offscreen/CSS/font chunks that may legitimately contain different code patterns)."
requirements-completed: []
# Metrics
duration: ~41 min
completed: 2026-05-21
---
# Phase 4 Plan 02: harden-clean-up-optional Summary
**Eliminated the SW chunk's `new Function` literal via a 4-layered mitigation (runtime queueMicrotask polyfill prelude + nodePolyfills exclude + resolve.alias stub + Rollup post-transform plugin), renamed `generate-icons.js` → `.cjs` for ESM/CJS disambiguation under package.json type:module, and pinned dead-code absence via regression-guard vitest — all under Plan 04-02's TDD-strict RED→GREEN contract.**
## Performance
- **Duration:** ~41 min (RED scaffold + GREEN initial Option a attempt + Option α empirical reversal + Option β implementation + UAT harness re-verification + SUMMARY)
- **Started:** 2026-05-21T12:36:43Z
- **Completed:** 2026-05-21T13:18:30Z
- **Tasks:** 2 (Wave 0 RED + Wave 1 GREEN per the plan's `tdd: true` frontmatter)
- **Files modified:** 5 (2 new build-gate tests + 1 new polyfill stub + vite.config.ts + src/background/index.ts + generate-icons rename + deferred-items.md closure-flip)
## Accomplishments
- **MV3 CSP-hardening Gate 2 polarity flipped** end-to-end: `grep -c 'new Function' dist/assets/index.ts-*.js` returns **0/0/0** (was 1 hit in `index.ts-8LkXuqac.js` pre-fix; documented since Plan 01-12 Wave 7).
- **ROADMAP SC #3 GREEN:** `node generate-icons.cjs` exits 0; old `generate-icons.js` no longer exists (renamed via `git mv` preserving history); no other references to the old `.js` path exist outside the `.planning/` audit trail.
- **ROADMAP SC #4 GREEN:** `permissions.request` regression-pinned absent from `src/` via `tests/build/dead-code-grep.test.ts` (GREEN-on-arrival; acts as future regression guard).
- **Plan 01-12 Wave 7 deferred-items entry CLOSED** end-to-end; `.planning/phases/01-stabilize-video-pipeline/deferred-items.md` appended with a multi-paragraph closure block documenting the 4-mechanism mitigation + the Option α reversal.
- **vitest baseline 180/180 → 183/183 GREEN on clean run** (+3 from this plan's 2 new test files; the 2 files contribute 3 it() blocks total — 1 build-prep gate + 1 grep gate in `no-new-function-in-sw-chunk.test.ts` + 1 grep gate in `dead-code-grep.test.ts`). Pre-existing intermittent flakes (`blob-url-download.test.ts` + `webm-remux.test.ts` + `webm-playback.test.ts`) per 04-01-SUMMARY Issues Encountered persist and are owned by Plan 04-03.
- **UAT harness 33/33 GREEN preserved** (REVISION iter-2 WARNING 1 empirical pin: `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exit 0 with verbatim `UAT harness: 33/33 assertions passed` stdout). Confirms JSZip's full zip-assembly pipeline operates correctly under the new bundle — the setimmediate polyfill replacement is observably transparent at the empirical SAVE→zip layer end-to-end.
- **Pre-checkpoint bundle gates 5/5 PASS** (Tier-1 FORBIDDEN_HOOK_STRINGS 13/13 GREEN; SW CSP-safety grep now 0 hits — polarity flipped; Node-globals + DOM-globals unchanged; manifest.json valid).
- **tsc-clean preserved** (`npx tsc --noEmit` exits 0).
## Task Commits
Each task was committed atomically per the plan's TDD cycle:
1. **Task 1: Wave 0 RED — build-gate grep tests**`630d40c` (test)
- 2 new test files at `tests/build/`: 1 RED gate (`no-new-function-in-sw-chunk` — 1 occurrence of `new Function` in the SW chunk) + 1 GREEN-on-arrival regression pin (`dead-code-grep` — 0 occurrences of `permissions.request` in `src/`).
- Acceptance: `grep -v '^//' tests/build/no-new-function-in-sw-chunk.test.ts | grep -c 'new Function'` returned 3 (≥2 required); `grep -v '^//' tests/build/dead-code-grep.test.ts | grep -c 'permissions.request'` returned 2 (≥2 required).
2. **Task 2: Wave 1 GREEN — setimmediate polyfill replaced + generate-icons.cjs + deferred-items closure**`f251297` (feat)
- 4 edits in vite.config.ts (node:url import + Plugin type; nodePolyfills.exclude; resolve.alias.setimmediate; Rollup post-transform plugin) + 17-LOC prelude in src/background/index.ts + new src/shared/setimmediate-stub.ts + `git mv generate-icons.js generate-icons.cjs` + deferred-items.md closure-flip block. The no-new-function RED gate from Task 1 flipped GREEN; all 6 plan-defined acceptance gates pass; UAT harness 33/33 GREEN preserved.
**Plan metadata commit:** to follow this SUMMARY landing.
## Files Created/Modified
- `tests/build/no-new-function-in-sw-chunk.test.ts` (NEW; 167 LOC) — Wave 0 RED build-gate grep test. Mirrors `tests/build/no-remote-fonts.test.ts` (Plan 01-12 analog). Narrows file walk to `dist/assets/index.ts-*.js` glob (BLOCKER 1 fix from plan-checker iter-1; the earlier `*-bg.js` pattern matched nothing). Includes glob-existence pre-gate (asserts ≥1 SW chunk match before the grep gate runs) so the grep can never silently no-op.
- `tests/build/dead-code-grep.test.ts` (NEW; 175 LOC) — Wave 0 GREEN-on-arrival regression pin for ROADMAP SC #4. Asserts `permissions.request` absence in `src/`; documents the offscreen-inline-string sub-test as delegated to `tests/build/no-remote-fonts.test.ts` (no single literal sentinel pinnable post-Plan-01-06 collapse).
- `src/shared/setimmediate-stub.ts` (NEW; 50 LOC) — minimal queueMicrotask-based polyfill installed as a side-effect import. The resolve.alias.setimmediate target. CSP-safe (contains NO `new Function`, NO `eval`).
- `vite.config.ts` (modified; 4 edits) — see "Task Commits" above for the diff anatomy.
- `src/background/index.ts` (modified; 17-LOC prelude inserted before first import) — typed widening cast for the polyfill assignment (no `as any` per CLAUDE.md naming guidance).
- `generate-icons.js → generate-icons.cjs` (renamed via `git mv`; 100% similarity preserved) — Node 14+ treats `.cjs` as CJS regardless of `package.json` "type":"module" per nodejs.org/api/packages.html#determining-module-system.
- `.planning/phases/01-stabilize-video-pipeline/deferred-items.md` (modified; ~45 LOC closure-flip block appended at EOF) — multi-paragraph "Resolved in Phase 4 Plan 04-02" block documenting the 4-mechanism mitigation + the Option α attempt-and-reversal.
## Decisions Made
**Option β over Option α (post-transform Rollup plugin over forcing JSZip's unbundled entry):** Option α was attempted first (force-redirect `import JSZip from 'jszip'` to `node_modules/jszip/lib/index.js` via `resolve.alias.jszip` so the internal `require("setimmediate")` chain passes through our `resolve.alias.setimmediate`). Empirically broke UAT harness A30+ assertions: the unbundled JSZip entry's transitive readable-stream-browser browser-field mapping did not propagate correctly through Vite's resolver, so JSZip's async zip-write pipeline silently produced an empty `events.json`. Option α was reverted in the same work session before commit. The Rollup `generateBundle` post-transform plugin (Option β) preserves JSZip's pre-bundled distribution verbatim (zip-write behavior unchanged) while excising the single offending text literal in any chunk that contains the JSZip-bundled setimmediate IIFE.
**4-layer defense-in-depth over a single mechanism:** The runtime prelude alone makes the SW chunk CSP-safe AT RUNTIME (JSZip's setimmediate IIFE's `if(!s.setImmediate){...}` guard skips the body once `globalThis.setImmediate` is installed by our prelude). But the static `new Function` literal remains in the bundle text, failing the Plan 04-02 build-gate test AND remaining a static-analysis red flag. Layering the post-transform plugin closes that gap surgically. The `nodePolyfills.exclude:['setimmediate']` + `resolve.alias.setimmediate → setimmediate-stub.ts` are belt-and-suspenders for any future direct `import 'setimmediate'` consumer that would bypass the JSZip path (would otherwise re-introduce the literal).
**`fileURLToPath(new URL('./...', import.meta.url))` over `path.resolve(__dirname, ...)` for resolve.alias paths:** `__dirname` is undefined under vite.config.ts's ESM mode (the project's `package.json` declares `"type": "module"` since Plan 01-06). The `import.meta.url`-based form is the canonical ESM idiom per Vite docs and matches the Node 20+ recommendation.
**`(I=function(){})` over `(I=()=>{})` for the post-transform replacement:** keeps the `function` keyword family of the surrounding pre-bundled CJS code (mangler-friendly; byte-parity within the noise envelope) and avoids introducing arrow-function syntax that JSZip's pre-bundled distribution doesn't otherwise use.
**Did NOT introduce a Vite plugin file at `tools/vite-plugins/`** — the `stripSetimmediateNewFunction()` function is defined inline in vite.config.ts with a multi-paragraph documentation block at the file top because (1) it's a single-purpose Plan 04-02-specific surgical fix, (2) the rationale needs to live with the configuration that activates it for future auditors, and (3) extracting it would create a new directory + import surface for a 30-LOC function. If a second similar surgical excision arises in a future plan, this is the canonical extract-to-module trigger.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] vite-plugin-node-polyfills `exclude` insufficient for JSZip's direct setimmediate require**
- **Found during:** Task 2 (first `npm run build` + grep verification after the plan-specified Option (a) edits landed)
- **Issue:** The plan's RESEARCH.md Q1 analysis specified that `nodePolyfills({ exclude: ['setimmediate'] })` would drop the setimmediate polyfill from the SW chunk, with JSZip falling back to its own inline MessageChannel/postMessage/setTimeout polyfill. Empirically, the `new Function` literal STILL appeared in the SW chunk after the plan-specified edits because JSZip's `package.json` `"browser"` field maps `./lib/index``./dist/jszip.min.js` (a pre-bundled CJS distribution with its own internal module registry containing the setimmediate polyfill at slot 54). The plugin's `exclude` only filters node-stdlib-browser-aliased polyfills; it cannot reach into JSZip's pre-bundled distribution.
- **Fix:** Added two additional mitigation layers: (1) `resolve.alias.setimmediate``src/shared/setimmediate-stub.ts` (catches any bare-specifier `import 'setimmediate'` consumer); (2) `stripSetimmediateNewFunction()` Rollup `generateBundle` post-transform plugin (excises the single offending text literal from JSZip's pre-bundled chunk after Vite has rolled it in).
- **Files modified:** `vite.config.ts` (added Plugin type import + node:url imports + 3-section plugin definition with full rationale comment), `src/shared/setimmediate-stub.ts` (NEW)
- **Verification:** Post-fix `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0/0/0 (was 1 in one of three chunks); `grep -c "I=function" dist/assets/index.ts-*.js` returns 1 in the SW chunk (the replacement landed exactly once); the Task 1 RED test flipped GREEN.
- **Committed in:** `f251297` (Task 2 commit; deviation absorbed into the Wave 1 GREEN landing because the fix completes the same charter — eliminating the SW-chunk `new Function` literal — as the plan-specified edits).
**2. [Rule 4-adjacent ARCHITECTURAL but handled inline] Option α attempt + reversal (force JSZip unbundled `lib/index.js`)**
- **Found during:** Task 2 (after the Rule 3 fix above attempted via Option α first — force JSZip's unbundled entry via `resolve.alias.jszip`)
- **Issue:** Option α was attempted as the first interception strategy for JSZip's pre-bundled setimmediate slot (rationale: forcing the unbundled `lib/index.js` entry would route every internal require through Vite's resolver, at which point the existing `resolve.alias.setimmediate` would intercept JSZip's `lib/utils.js:7` `require("setimmediate")` and substitute the CSP-safe stub). UAT harness regression: A30+ assertions failed with `userEvents.length=0` in the produced zip despite the content-script-side events firing correctly and the SW logging `✓ Received 4 rrweb events, 5 user events` + `✓ Added user events: 5 events, 1199 bytes`. Root cause: JSZip's unbundled `lib/index.js` entry's transitive `readable-stream``readable-stream-browser` browser-field mapping did not propagate correctly through Vite's resolver, breaking JSZip's async zip-write pipeline at the StreamHelper layer.
- **Fix:** Reverted the `resolve.alias.jszip` entry; pivoted to the post-transform plugin (Option β) which preserves JSZip's pre-bundled distribution verbatim while excising the offending text literal post-bundle.
- **Files modified:** `vite.config.ts` (reverted the jszip alias addition; added the stripSetimmediateNewFunction plugin instead)
- **Verification:** Post-pivot UAT harness ran end-to-end at 33/33 GREEN (`grep -c 'UAT harness: 33/33 assertions passed' /tmp/04-02-uat-2.log` returned 1) — JSZip's zip-write pipeline restored verbatim.
- **Committed in:** `f251297` (Task 2 commit; both the Rule 3 fix above AND this Rule 4-adjacent pivot landed in the same commit because they're parts of the same coherent multi-mechanism landing per RESEARCH Q1 acceptance "must land coherently in the same plan task").
- **Decision rationale documented inline:** the Task 2 commit body has a dedicated "Architecture decision log" paragraph explaining the Option α attempt + empirical regression + Option β pivot in full so future Phase 4+ executors investigating similar transitive-polyfill issues have the prior-art breadcrumb.
---
**Total deviations:** 2 auto-fixed (1 blocking, 1 Rule 4-adjacent handled inline as part of the same Wave 1 coherent landing).
**Impact on plan:** Zero scope creep. The plan's must_have #1 (SW chunk `new Function` count flipped 1→0) is the same charter; the deviations were about HOW to achieve it (1 mechanism became 4 layered mechanisms; 1 attempted approach was empirically falsified and replaced inline). The plan's must_have #2 (JSZip MessageChannel/postMessage/setTimeout fallback chain handles JSZip's needs cleanly post-polyfill) is functionally preserved end-to-end — verified by UAT harness 33/33 GREEN — though architecturally the fallback chain is never actually engaged at runtime (our prelude pre-seeds `globalThis.setImmediate`, so JSZip's setimmediate IIFE skips entirely; the fallback chain is the bundle-time dead-code branch that gets excised by the post-transform plugin).
## Issues Encountered
**Puppeteer Chrome binary missing at first UAT harness invocation:** `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` on the first attempt errored with `Could not find Chrome (ver. 148.0.7778.167)`. Resolved via `npx puppeteer browsers install chrome` (one-time install; cached under `/home/parf/.cache/puppeteer`). Not a regression; a fresh-environment side-effect of the Phase 4 first-time UAT invocation on this machine. Plan 04-01 had not exercised the UAT harness (its work was content-script unit tests + build-gate tests only); Plan 04-02 is the first Phase 4 plan to require Puppeteer Chrome empirically per the REVISION iter-2 WARNING 1 contract.
**Pre-existing vitest flakes** — same 3 documented in 04-01-SUMMARY "Issues Encountered" (`tests/background/blob-url-download.test.ts` 5000ms timeout race, `tests/background/webm-remux.test.ts` ffprobe frame count, `tests/offscreen/webm-playback.test.ts` ffmpeg dry-run). Intermittent across baseline runs; characterized as "1-3 of these 3 fail per run; the same 3 are owned by Plan 04-03". Plan 04-02 introduces no new flakes — verified by a clean 183/183 GREEN run.
## Pre-Checkpoint Bundle Gates
Per saved memory `feedback-pre-checkpoint-bundle-gates.md` (5/5 standard inventory):
1. **Tier-1 FORBIDDEN_HOOK_STRINGS**`tests/background/no-test-hooks-in-prod-bundle.test.ts` 13/13 GREEN; inventory unchanged at 12 strings (Plan 04-02 added no harness hooks; pure source-side polyfill + config polish).
2. **SW CSP-safety grep**`grep -E 'new Function|\beval\(' dist/assets/index.ts-*.js` returns **0 hits across all three SW chunks** (loader-D5qBgxJ_.js + D0uUn23q.js + DfBxWCT9.js). **Polarity flipped from 1 documented exception (Plan 01-12 Wave 7 disclosure) to 0 hits** — the Plan 04-02 closure charter is empirically discharged.
3. **Node-globals grep**`Buffer.copy / .isView / .length / .push / .shift / .slice / .write` in SW chunk — all from JSZip internals; unchanged from 04-01-SUMMARY Bundle Gate 3.
4. **DOM-globals grep**`document.createElement / .createTextNode / .documentElement / .F` + `window.Math / .console / .localStorage / .process` in SW chunk — pre-existing shimmed-DOM references inside JSZip's text encoder fallback paths; unchanged from 04-01-SUMMARY Bundle Gate 4.
5. **manifest.json** — present at `dist/manifest.json`; `manifest_version: 3`; `name: "__MSG_extName__"` (chrome.i18n message resolution intact). Plan 04-02 did NOT touch `_locales/` so en↔ru parity is untouched.
## Empirical UAT Harness Pin (REVISION iter-2 WARNING 1)
```
$ HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat 2>&1 | tail -3
[PASS] A32
========================================================================
UAT harness: 33/33 assertions passed
$ grep -c 'UAT harness: 33/33 assertions passed' /tmp/04-02-uat-2.log
1
```
JSZip's full zip-assembly pipeline (A24-A32 inclusive — exercising MediaRecorder segments via base64 port wire + remux + zip assembly + chrome.downloads + events.json + meta.json + screenshot) operates correctly under the new bundle. The setimmediate polyfill replacement is observably transparent at the empirical SAVE→zip layer.
## Self-Check
- **Files created:**
- tests/build/no-new-function-in-sw-chunk.test.ts → FOUND (verified via `test -f`)
- tests/build/dead-code-grep.test.ts → FOUND
- src/shared/setimmediate-stub.ts → FOUND
- **Files modified:**
- vite.config.ts → FOUND (diff shows 4 edits per "Files Created/Modified" above)
- src/background/index.ts → FOUND (17-LOC prelude before first import)
- .planning/phases/01-stabilize-video-pipeline/deferred-items.md → FOUND (closure block appended)
- **Files renamed:**
- generate-icons.js → generate-icons.cjs → FOUND (`test ! -e generate-icons.js && test -f generate-icons.cjs` both pass)
- **Commits:**
- 630d40c → FOUND (test(04-02): Wave 0 RED)
- f251297 → FOUND (feat(04-02): Wave 1 GREEN)
- **Verification commands all green:**
- `npm run build` → exit 0
- `npx tsc --noEmit` → exit 0
- `grep -c 'new Function' dist/assets/index.ts-*.js` → 0/0/0 (was 1)
- `grep -rn 'permissions.request' src/` → exit 1 (no matches; correct)
- `node generate-icons.cjs` → exit 0
- `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run` → 3/3 GREEN
- `npm test -- --run` (clean run) → 183/183 GREEN
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` → 33/33 GREEN; `grep -c 'UAT harness: 33/33 assertions passed' /tmp/04-02-uat-2.log` returns 1
## Self-Check: PASSED
## Next Phase Readiness
- Plan 04-03 (flake stabilization: A29 cs-injection-world rewrite + parallel-vitest race + 2 ffprobe/ffmpeg flakes — `blob-url-download` + `webm-remux` + `webm-playback`) is **unblocked**. The pre-existing flakes persist as the same 3 intermittent items documented in 04-01-SUMMARY Issues Encountered. Plan 04-02 introduces zero new flakes.
- Plan 04-02 closes Plan 01-12 Wave 7's setimmediate disclosure end-to-end (`.planning/phases/01-stabilize-video-pipeline/deferred-items.md` now has a multi-paragraph "Resolved in Phase 4 Plan 04-02" closure block).
- ROADMAP success criteria status update: SC #3 (generate-icons ESM/CJS) — **GREEN closed**; SC #4 (dead-code grep `permissions.request`) — **GREEN regression-pinned**. SC #1 + SC #2 still owned by future Phase 4 plans (04-03 / 04-04 per the CONTEXT.md suggested grouping).
- The layered transitive-polyfill CSP-hardening pattern (runtime guard + bundler exclude + bundler alias + Rollup post-transform) is now an **established Phase 4 pattern**, available for future plans that encounter similar pre-bundled-distribution interception challenges. The pattern's rationale + Option α/β trade-off is documented inline in vite.config.ts AND in the Plan 01-12 deferred-items.md closure block.
---
*Phase: 04-harden-clean-up-optional*
*Completed: 2026-05-21*