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

27 KiB
Raw Blame History

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 tests630d40c (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 closuref251297 (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.setimmediatesrc/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-streamreadable-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_STRINGStests/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 grepgrep -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 grepBuffer.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 grepdocument.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