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>
27 KiB
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: truefrontmatter) - 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-*.jsreturns 0/0/0 (was 1 hit inindex.ts-8LkXuqac.jspre-fix; documented since Plan 01-12 Wave 7). - ROADMAP SC #3 GREEN:
node generate-icons.cjsexits 0; oldgenerate-icons.jsno longer exists (renamed viagit mvpreserving history); no other references to the old.jspath exist outside the.planning/audit trail. - ROADMAP SC #4 GREEN:
permissions.requestregression-pinned absent fromsrc/viatests/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.mdappended 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 indead-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:uatexit 0 with verbatimUAT harness: 33/33 assertions passedstdout). 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 --noEmitexits 0).
Task Commits
Each task was committed atomically per the plan's TDD cycle:
- 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 ofnew Functionin the SW chunk) + 1 GREEN-on-arrival regression pin (dead-code-grep— 0 occurrences ofpermissions.requestinsrc/). - 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 new test files at
- 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.
- 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 +
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. Mirrorstests/build/no-remote-fonts.test.ts(Plan 01-12 analog). Narrows file walk todist/assets/index.ts-*.jsglob (BLOCKER 1 fix from plan-checker iter-1; the earlier*-bg.jspattern 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. Assertspermissions.requestabsence insrc/; documents the offscreen-inline-string sub-test as delegated totests/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 NOnew Function, NOeval).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 (noas anyper CLAUDE.md naming guidance).generate-icons.js → generate-icons.cjs(renamed viagit mv; 100% similarity preserved) — Node 14+ treats.cjsas CJS regardless ofpackage.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, thenew Functionliteral STILL appeared in the SW chunk after the plan-specified edits because JSZip'spackage.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'sexcludeonly 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-specifierimport 'setimmediate'consumer); (2)stripSetimmediateNewFunction()RollupgenerateBundlepost-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-*.jsreturns 0/0/0 (was 1 in one of three chunks);grep -c "I=function" dist/assets/index.ts-*.jsreturns 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-chunknew Functionliteral — 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.jsentry would route every internal require through Vite's resolver, at which point the existingresolve.alias.setimmediatewould intercept JSZip'slib/utils.js:7require("setimmediate")and substitute the CSP-safe stub). UAT harness regression: A30+ assertions failed withuserEvents.length=0in 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 unbundledlib/index.jsentry's transitivereadable-stream→readable-stream-browserbrowser-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.jszipentry; 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.logreturned 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):
- Tier-1 FORBIDDEN_HOOK_STRINGS —
tests/background/no-test-hooks-in-prod-bundle.test.ts13/13 GREEN; inventory unchanged at 12 strings (Plan 04-02 added no harness hooks; pure source-side polyfill + config polish). - SW CSP-safety grep —
grep -E 'new Function|\beval\(' dist/assets/index.ts-*.jsreturns 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. - Node-globals grep —
Buffer.copy / .isView / .length / .push / .shift / .slice / .writein SW chunk — all from JSZip internals; unchanged from 04-01-SUMMARY Bundle Gate 3. - DOM-globals grep —
document.createElement / .createTextNode / .documentElement / .F+window.Math / .console / .localStorage / .processin SW chunk — pre-existing shimmed-DOM references inside JSZip's text encoder fallback paths; unchanged from 04-01-SUMMARY Bundle Gate 4. - 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
- tests/build/no-new-function-in-sw-chunk.test.ts → FOUND (verified via
- 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.cjsboth pass)
- generate-icons.js → generate-icons.cjs → FOUND (
- Commits:
- Verification commands all green:
npm run build→ exit 0npx tsc --noEmit→ exit 0grep -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 0npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run→ 3/3 GREENnpm test -- --run(clean run) → 183/183 GREENHEADLESS=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.logreturns 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.mdnow 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