---
phase: 04
slug: harden-clean-up-optional
plan: 02
type: tdd
wave: 1
depends_on: []
files_modified:
  - vite.config.ts
  - src/background/index.ts
  - generate-icons.js
  - generate-icons.cjs
  - tests/build/no-new-function-in-sw-chunk.test.ts
  - tests/build/dead-code-grep.test.ts
  - .planning/phases/01-stabilize-video-pipeline/deferred-items.md
autonomous: true
requirements: []
tags:
  - build-hygiene
  - csp-hardening
  - setimmediate-polyfill
  - dead-code-grep
  - generate-icons-cjs
  - roadmap-sc-3
  - roadmap-sc-4
  - tdd
  - charter-d-p4-01
user_setup: []
must_haves:
  truths:
    - "grep -c 'new Function' dist/assets/index.ts-*.js returns 0 (was 1 — the setimmediate polyfill literal)"
    - "JSZip's inline polyfill chain (MessageChannel/postMessage/setTimeout) handles setImmediate fallback cleanly in the SW chunk after the dependency-side polyfill is excluded"
    - "ripgrep 'permissions\\.request' src/ returns 0 hits (was removed in Phase 1 Plan 01-05; regression-pinned)"
    - "node generate-icons.cjs exits 0 under package.json type: module (CJS-explicit via .cjs extension)"
    - "vitest baseline +2 (no-new-function-in-sw-chunk + dead-code-grep tests) GREEN"
    - "Pre-checkpoint bundle Gate 2 polarity flipped (1 hit → 0 hits for 'new Function' in dist/assets/index.ts-*.js)"
    - "UAT harness 33/33 GREEN preserved post-polyfill-replacement (REVISION iter-2 WARNING 1 — JSZip's MessageChannel/postMessage/setTimeout fallback chain verified empirically end-to-end at the SAVE→zip layer)"
  artifacts:
    - path: "tests/build/no-new-function-in-sw-chunk.test.ts"
      provides: "Wave 0 RED — grep gate pinning 0 hits of 'new Function' in SW chunk after Plan 04-02 polyfill replacement"
      contains: "new Function"
      min_lines: 50
    - path: "tests/build/dead-code-grep.test.ts"
      provides: "Wave 0 GREEN-on-arrival — regression pin for ROADMAP SC #4 (permissions.request removed in Plan 01-05)"
      contains: "permissions.request"
      min_lines: 40
    - path: "vite.config.ts"
      provides: "nodePolyfills config with exclude: ['setimmediate'] added"
      contains: "exclude: ['setimmediate']"
    - path: "src/background/index.ts"
      provides: "Top-of-module queueMicrotask-based setImmediate polyfill prelude (4 LOC; inserted BEFORE first import)"
      contains: "queueMicrotask"
    - path: "generate-icons.cjs"
      provides: "Renamed from generate-icons.js to disambiguate CJS under package.json type: module"
      contains: "require"
    - path: ".planning/phases/01-stabilize-video-pipeline/deferred-items.md"
      provides: "Appended closure-flip block — 'Resolved in Phase 4 Plan 04-02' (referenced from existing Plan 01-12 Wave 7 disclosure block)"
      contains: "Resolved in Phase 4 Plan 04-02"
  key_links:
    - from: "vite.config.ts nodePolyfills exclude: ['setimmediate']"
      to: "dist/assets/index.ts-*.js (SW chunk grep)"
      via: "vite build pipeline excludes setimmediate transitive dep"
      pattern: "exclude: \\['setimmediate'\\]"
    - from: "src/background/index.ts top-of-module setImmediate prelude"
      to: "JSZip + transitive deps that call setImmediate(fn)"
      via: "globalThis.setImmediate = (fn, ...args) => queueMicrotask(() => fn(...args))"
      pattern: "queueMicrotask\\(\\(\\) => fn\\(\\.\\.\\.args\\)\\)"
    - from: "tests/build/no-new-function-in-sw-chunk.test.ts"
      to: "dist/assets/index.ts-<hash>.js (post-build grep)"
      via: "execFile npm run build + readFileSync + countOccurrences"
      pattern: "FORBIDDEN_SW_CSP_PATTERNS"
---

<objective>
Three independent build-hygiene fixes consolidated into one plan because they share the test-scaffold pattern (build-gate grep tests) AND none touch harness or content-script files (zero conflict with Plan 04-01):

1. **setimmediate polyfill replacement (RESEARCH Q1, Option a):** Drop `new Function` from the SW chunk by adding `exclude: ['setimmediate']` to `vite-plugin-node-polyfills` config + a 4-LOC `queueMicrotask`-based inline polyfill prelude at the top of `src/background/index.ts`. The plugin's transitive `setimmediate` package ships a CSP-unsafe `new Function(string)` fallback for the never-called `setImmediate(string)` form; JSZip (the only legitimate consumer in our bundle) falls back to its own inline MessageChannel/postMessage/setTimeout polyfill chain when `globalThis.setImmediate` is unset, so the explicit fast-path is a strict superset of behavior.

2. **Dead-code grep (ROADMAP SC #4):** `permissions.request` was removed in Phase 1 Plan 01-05; pin the absence via a new vitest grep test so a future regression breaks the build.

3. **generate-icons ESM/CJS (ROADMAP SC #3):** `package.json` declares `"type": "module"`, so `.js` files are parsed as ESM, but `generate-icons.js` uses `require('fs')` (CJS). Rename `generate-icons.js` → `generate-icons.cjs` (Node treats `.cjs` as CJS regardless of the enclosing type per Node packages docs). Single-file rename; no code change.

TDD-strict: Tests 1 + 2 are Wave 0 RED-by-build-state (test 1 is RED until the polyfill landed; test 2 is GREEN-on-arrival since `permissions.request` was already removed in Phase 1). Test 1 flips RED→GREEN after the polyfill replacement; test 2 acts as regression pin. The setimmediate fix has both `vite.config.ts` config change AND `src/background/index.ts` prelude change — they must land coherently in one commit.

Purpose: Each item closes an audit residual without touching app behavior. (1) tightens MV3 CSP posture by removing the static-analysis red flag in production code. (2) prevents accidental re-introduction of the dead `permissions.request` API call. (3) makes `node generate-icons.cjs` work under the project's ESM-by-default module setting.

Output: 2 new test files at `tests/build/`; 1-line addition to `vite.config.ts`; 8-line prelude at top of `src/background/index.ts`; 1 file rename `generate-icons.js` → `generate-icons.cjs`; 1-block append to `deferred-items.md` flipping the Plan 01-12 Wave 7 setimmediate disclosure to "Resolved in Phase 4 Plan 04-02".
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
@.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
@.planning/phases/01-stabilize-video-pipeline/deferred-items.md

# Source files — locus of the build-hygiene edits
@vite.config.ts
@src/background/index.ts
@generate-icons.js
@package.json

# Analog test scaffold to mirror — build-gate grep test pattern
@tests/build/no-remote-fonts.test.ts

<interfaces>
<!-- Key shapes the executor consumes directly. Extracted from codebase 2026-05-21. -->

From vite.config.ts (current nodePolyfills config — lines 14-22):
```typescript
nodePolyfills({
  include: ['buffer'],
  globals: {
    Buffer: true,
    global: false,
    process: false,
  },
  protocolImports: false,
}),
```

After Phase 4 Plan 04-02 (1-line addition):
```typescript
nodePolyfills({
  include: ['buffer'],
  exclude: ['setimmediate'],   // Plan 04-02 CSP hardening — drops `new Function` from SW chunk
  globals: {
    Buffer: true,
    global: false,
    process: false,
  },
  protocolImports: false,
}),
```

From src/background/index.ts:1 (current top — first line is the Logger import):
```typescript
import { Logger } from '../shared/logger';
import { base64ToBlob, blobToBase64 } from '../shared/binary';
// ... long import block
```

After Phase 4 Plan 04-02 (8-line prelude INSERTED BEFORE first import):
```typescript
// Plan 04-02 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
```

From generate-icons.js:1 (current — CJS smoking gun):
```javascript
const fs = require('fs');
```

After Phase 4 Plan 04-02:
- File renamed via `git mv generate-icons.js generate-icons.cjs` (no code change).
- Node 14+ treats `.cjs` as CJS regardless of enclosing `"type": "module"` per https://nodejs.org/api/packages.html#determining-module-system
- Verify no scripts in package.json reference the old `.js` name; verify no docs reference the old `.js` name (`rg 'generate-icons\\.js' .` before the rename).

From tests/build/no-remote-fonts.test.ts (existing test scaffold — full 145 lines):
- Imports: `execFile` + `existsSync` + `readFileSync` + `readdirSync` + `statSync` + `resolve` + `promisify` + vitest
- Pattern: build → recursive walk → countOccurrences → describe block with one it() per forbidden string
- `runProductionBuild()`: `execFileAsync('npm', ['run', 'build'], { timeout: 90_000 })`
- Skip gate: `if (process.env.SKIP_BUILD !== '1') { await runProductionBuild(); }` — re-uses existing built dist/ when SKIP_BUILD=1 (developer-velocity escape hatch)
- The Plan 04-02 RED test inherits the same scaffold; SCOPES the walk to `dist/assets/` filtered by `^index\\.ts-.*\\.js$` regex (the SW chunk only).

UAT harness stdout format (REVISION iter-2 — WARNING 1 mitigation):
- The orchestrator at tests/uat/harness.test.ts:537 emits exactly `UAT harness: 33/33 assertions passed\n` (verified 2026-05-21 via grep). Task 2 `<verify><automated>` greps for this canonical line to confirm post-polyfill JSZip behavior is preserved end-to-end at the SAVE→zip layer.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Wave 0 RED — build-gate grep tests (no-new-function-in-sw-chunk + dead-code-grep)</name>
  <files>tests/build/no-new-function-in-sw-chunk.test.ts, tests/build/dead-code-grep.test.ts</files>
  <read_first>tests/build/no-remote-fonts.test.ts, .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md (sections "tests/build/no-new-function-in-sw-chunk.test.ts" + "tests/build/dead-code-grep.test.ts")</read_first>
  <behavior>
    no-new-function-in-sw-chunk.test.ts (1 build-prep test + 1 grep test):
    - it: 'npm run build completes and dist/assets/ exists' → SKIP_BUILD=1 escape hatch + production-build invocation
    - it: 'dist/assets/index.ts-*.js does not contain "new Function" (Plan 04-02 CSP hardening — Q1 finding)' → RED today (existing setimmediate polyfill literal); GREEN after Task 2 lands the polyfill replacement.

    dead-code-grep.test.ts (1 multi-pattern grep test):
    - it: 'src/ does not contain "permissions.request" (removed Phase 1 Plan 01-05)' → GREEN-on-arrival; acts as regression pin.
    - it: 'vite.config.ts does not contain the offscreen inline-string sentinel (removed Phase 1 Plan 01-06)' → if the planner can pin a specific sentinel from pre-01-06 vite.config.ts (e.g., a unique HTML literal the inline plugin emitted), include this; if not pinnable to a single string, drop this sub-test and document in the plan SUMMARY that ROADMAP SC #4's vite.config.ts-side audit is regression-pinned at the offscreen-related Vite plugin layer indirectly via `tests/build/no-remote-fonts.test.ts` (which audits the full dist/ for `googleapis` etc. — empirically covers the inline-string removal).
  </behavior>
  <action>
    1. Read `tests/build/no-remote-fonts.test.ts` (~145 lines) once. Extract the imports + helper signatures + describe-block scaffold.
    2. Create `tests/build/no-new-function-in-sw-chunk.test.ts`:
       - Mirror the imports verbatim (execFile + node:fs + node:path + node:util + vitest).
       - Define `const FORBIDDEN_SW_CSP_PATTERNS: ReadonlyArray<string> = ['new Function'];`
       - Define `const DIST_ASSETS_DIR = resolvePath(process.cwd(), 'dist', 'assets');`
       - Define a helper `listSwChunkFiles(): ReadonlyArray<string>` that returns `readdirSync(DIST_ASSETS_DIR)` filtered by `/^index\.ts-.*\.js$/`. The SW entry is bundled to a hashed file matching this regex (verify post-build: `ls dist/assets/index.ts-*.js`).
       - Define `countOccurrencesInFile` helper identical to the analog.
       - `describe('production SW chunk has no `new Function` literal (MV3 CSP hardening — Plan 04-02 / RESEARCH Q1)', ...)` with one it() for the build-prep gate + one it() per needle in FORBIDDEN_SW_CSP_PATTERNS.
       - Set SKIP_BUILD env-gate per the analog.

    3. Create `tests/build/dead-code-grep.test.ts`:
       - Mirror imports + helpers (NO build invocation — this test reads `src/` and `vite.config.ts` directly).
       - Define `const FORBIDDEN_DEAD_CODE: ReadonlyArray<{ readonly needle: string; readonly searchPaths: ReadonlyArray<string>; readonly rationale: string }> = [...]` with the entries from 04-PATTERNS.md section.
       - One it() per entry; each runs `rg <needle> <paths>` (via execFile spawn of `rg` OR a recursive readdir+filter in pure Node — analog uses pure Node which is preferred for portability).
       - The `permissions.request` test is GREEN today (regression pin). If the planner empirically determines a vite.config.ts offscreen-inline-string sentinel CAN be pinned, add the second sub-test; otherwise document the rationale in the SUMMARY and skip.

    Filter-pipeline form (no for-of with continue) per CLAUDE.md. Absolute imports.

    RED gate: `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run` — the dead-code-grep test is GREEN; the no-new-function test is RED ('new Function' count = 1 in current dist/).
  </action>
  <verify>
    <automated>npm run build && npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run 2>&1 | tee /tmp/04-02-task-1.log; grep -cE 'FAIL|✗' /tmp/04-02-task-1.log; grep -cE 'PASS|✓' /tmp/04-02-task-1.log</automated>
  </verify>
  <acceptance_criteria>
    - File `tests/build/no-new-function-in-sw-chunk.test.ts` exists with ≥ 2 it() blocks (1 build-prep + 1+ grep).
    - File `tests/build/dead-code-grep.test.ts` exists with ≥ 1 it() block.
    - Running both files: `no-new-function-in-sw-chunk.test.ts` has at least 1 RED (the 'new Function' grep); `dead-code-grep.test.ts` is fully GREEN.
    - `grep -v '^#' tests/build/no-new-function-in-sw-chunk.test.ts | grep -c 'new Function'` returns ≥ 2 (1 in FORBIDDEN array + 1 in the it() description).
    - `grep -v '^#' tests/build/dead-code-grep.test.ts | grep -c 'permissions.request'` returns ≥ 2 (1 in FORBIDDEN_DEAD_CODE array + 1 in the it() description).
  </acceptance_criteria>
  <done>2 build-gate test files committed; 1 RED + 1 GREEN-on-arrival. Atomic commit: `test(04-02): Wave 0 — no-new-function-in-sw-chunk RED + dead-code-grep regression pin`.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Wave 1 GREEN — setimmediate polyfill replacement (vite.config.ts + src/background/index.ts prelude) + generate-icons.js → .cjs rename + deferred-items.md flip</name>
  <files>vite.config.ts, src/background/index.ts, generate-icons.js, generate-icons.cjs, .planning/phases/01-stabilize-video-pipeline/deferred-items.md</files>
  <read_first>vite.config.ts, src/background/index.ts (top-of-module — lines 1-30), generate-icons.js, .planning/phases/01-stabilize-video-pipeline/deferred-items.md, package.json (scripts section + type field)</read_first>
  <behavior>
    All 3 changes land in one commit per "8-line config change must land coherently with the SW-entry prelude" rule (RESEARCH Q1 acceptance gate). Post-fix:
    - `npm run build` exit 0
    - `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0 (was 1)
    - `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts --run` GREEN (the RED from Task 1 flips)
    - `node generate-icons.cjs` exit 0
    - `node generate-icons.js` exit 1 (the file no longer exists; this is the desired state — verifies the rename worked)
    - UAT harness 33/33 GREEN preserved (JSZip falls back to inline polyfill chain cleanly — RESEARCH Q1 Assumption A2)
    - vitest 171→179 (per Plan 04-01) → 181 GREEN (+2 from this plan's Task 1)
  </behavior>
  <action>
    Edit 1 — vite.config.ts (1-line addition):
    - Read full file (~75 lines).
    - Use Edit tool with surrounding context to insert `    exclude: ['setimmediate'],   // Plan 04-02 CSP hardening — drops `new Function` from SW chunk` between the existing `include: ['buffer'],` line and the `globals: {` block. Preserve indentation (4 spaces for the array entry).

    Edit 2 — src/background/index.ts top-of-module prelude:
    - Read lines 1-30 (the import block top).
    - Use Edit tool to insert the 11-line prelude (comment block + if-guard + assignment) BEFORE the existing `import { Logger } from '../shared/logger';` line. Match the exact prose from the `<interfaces>` block above (the executor can copy-paste verbatim).
    - Verify the typed widening cast uses inline interface intersection (no `as any`) per CLAUDE.md.
    - Run `npx tsc --noEmit` to confirm tsc-clean.

    Edit 3 — generate-icons.js → generate-icons.cjs:
    - First grep for references: `rg 'generate-icons\\.js' . --files-with-matches` to enumerate any package.json scripts, README mentions, CLAUDE.md mentions, etc.
    - `git mv generate-icons.js generate-icons.cjs` (preserves git history).
    - For each reference found by the grep, use Edit tool to replace `generate-icons.js` → `generate-icons.cjs` in that file (likely package.json scripts; possibly README).
    - Verify: `node generate-icons.cjs` exits 0 (no Node ERR_REQUIRE_ESM error — that error was the smoking gun this rename fixes).

    Edit 4 — deferred-items.md flip:
    - Read full file (~42 lines).
    - Append a new bullet at the end of the existing Plan 01-12 Wave 7 entry (after line 42), citing the Plan 04-02 commit hash (placeholder `<HASH>` — replace post-commit; the SUMMARY task will substitute the real hash). Use the exact prose from 04-PATTERNS.md section "deferred-items.md flip".

    Coherence check: all 4 edits + the rename land in ONE commit per RESEARCH Q1 acceptance ("must land coherently in the same plan task"). If TypeScript errors surface during Edit 2, fix the typed cast iteratively, but do NOT split into a separate commit. Pre-checkpoint bundle Gate 2 polarity flip (from 1 → 0 hits of `new Function` in `dist/assets/index.ts-*.js`) is the post-Edit-1+2 invariant; the bundle-gate test from Task 1 verifies it.

    Run the focused test: `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run` — expect 100% GREEN. Run the full vitest: `npm test -- --run` — expect 181 GREEN (or post-Plan-04-01 baseline + 2). Run the UAT harness: `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` — expect 33/33 GREEN (the SKIP_PROD_REBUILD=0 forces a rebuild against the new vite config + prelude so the UAT runs against the post-fix bundle).
  </action>
  <verify>
    <automated>npm run build && grep -c 'new Function' dist/assets/index.ts-*.js | head -1; node generate-icons.cjs; npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run; npx tsc --noEmit && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat 2>&1 | tee /tmp/04-02-task-2-uat.log | tail -3; grep -c 'UAT harness: 33/33 assertions passed' /tmp/04-02-task-2-uat.log</automated>
  </verify>
  <acceptance_criteria>
    - `npm run build` exits 0; `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0 (was 1).
    - `node generate-icons.cjs` exits 0; `test ! -e generate-icons.js` (the old file no longer exists; rename via git mv preserves history).
    - `npm test -- tests/build/ --run` exits 0; all Plan 04-02 Task 1 tests GREEN (the no-new-function RED from Task 1 flipped GREEN).
    - `npx tsc --noEmit` exits 0 (the typed widening cast at the prelude is tsc-clean).
    - `grep -c "exclude: \\['setimmediate'\\]" vite.config.ts` returns 1.
    - `grep -c "queueMicrotask" src/background/index.ts | head -1` returns ≥ 1 (the polyfill assignment).
    - `grep -c "Resolved in Phase 4 Plan 04-02" .planning/phases/01-stabilize-video-pipeline/deferred-items.md` returns ≥ 1.
    - Full vitest passes: `npm test -- --run` exits 0 (Plan 04-01 baseline +2 from this plan = ≥ 181 GREEN).
    - UAT harness 33/33 GREEN preserved (REVISION iter-2 — WARNING 1): `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 AND stdout contains `UAT harness: 33/33 assertions passed`; verified by `grep -c 'UAT harness: 33/33 assertions passed' /tmp/04-02-task-2-uat.log` returning 1. This confirms JSZip fallback (MessageChannel/postMessage/setTimeout) works correctly post-polyfill-removal at the empirical SAVE→zip layer.
  </acceptance_criteria>
  <done>Polyfill replacement landed; SW chunk grep flipped 1 → 0; generate-icons CJS-renamed; deferred-items.md closure-flipped. Atomic commit: `feat(04-02): Wave 1 — setimmediate polyfill replaced + generate-icons.cjs + deferred-items closure`.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Vite build pipeline → SW chunk → MV3 CSP | The SW chunk is loaded under MV3's `script-src 'self'` CSP; `new Function(string)` literals are a static-analysis red flag for future tighter CSP (MV3 default does allow it but auditors will flag) |
| JSZip dep → setImmediate global → SW realm | JSZip calls `setImmediate(fn)` (function form only — never string form); the polyfill chain has to provide a callable setImmediate; if the global is undefined, JSZip falls back to its own MessageChannel/postMessage/setTimeout polyfill (verified RESEARCH Q1) |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-04-02-01 | Elevation of Privilege | SW chunk `new Function(string)` literal — a static-analysis red flag for tighter future CSP and a security-audit eyebrow-raiser even under current MV3 CSP defaults | mitigate | Replace transitive `setimmediate` polyfill with explicit `queueMicrotask`-based polyfill in SW entry; verifiable by grep against built dist/. Reversible by git revert. |
| T-04-02-02 | DoS (functional) | JSZip relies on setImmediate to yield between zip-entry writes; if our explicit polyfill `(fn, ...args) => queueMicrotask(() => fn(...args))` is incompatible with JSZip's internal use, the zip-assembly could starve or deadlock | accept (verified by UAT) | The polyfill matches JSZip's signature expectation (function + variadic args); UAT harness 33/33 GREEN under the new bundle confirms behavior preserved (A24 specifically tests SAVE→zip; A28/A29/A30/A31 all exercise the zip-assembly path empirically). REVISION iter-2 WARNING 1: Task 2 `<verify><automated>` now greps for `UAT harness: 33/33 assertions passed` in stdout so the gate is empirically pinned, not merely asserted in `<acceptance_criteria>`. |
| T-04-02-03 | Information Disclosure | leftover `permissions.request` literal in src/ could give a future audit the impression the codebase still relies on the deleted permission flow, masking the post-01-05 architecture | mitigate (regression pin) | dead-code-grep.test.ts pins absence — re-introduction breaks CI |
| T-04-02-04 | Tampering | `.cjs` extension override applies only to the renamed file; future contributors writing `.js` files under the same project type:module would re-introduce the ESM/CJS error | accept | Convention is documented in the SUMMARY; CI catches future `require(` in `.js` files via `npx tsc --noEmit` at build time + the explicit `node generate-icons.cjs` invocation in any future ROADMAP backfill |
</threat_model>

<verification>
- `npm run build` exits 0 with no new warnings.
- `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0 (was 1; Gate 2 polarity flipped).
- `grep -c 'permissions.request' src/` returns 0 (regression pin).
- `node generate-icons.cjs` exits 0.
- vitest baseline +2 to ≥ 181 (Plan 04-01 baseline + 2 new build-gate tests).
- tsc-clean.
- UAT harness 33/33 GREEN preserved (validates JSZip fallback under the new polyfill regime) — REVISION iter-2 WARNING 1: grep `UAT harness: 33/33 assertions passed` in `<verify><automated>` confirms empirically.
- Pre-checkpoint bundle Gate 2 (SW CSP-safety: `grep -rn "new Function\\|eval(" dist/assets/`) returns 0 hits (was 1 documented exception).
</verification>

<success_criteria>
- 2 new Wave 0 test files committed (Task 1).
- vite.config.ts + src/background/index.ts + generate-icons rename + deferred-items.md flipped in one Wave 1 commit (Task 2).
- ROADMAP SC #3 (generate-icons ESM/CJS) GREEN — `node generate-icons.cjs` exits 0.
- ROADMAP SC #4 (dead-code grep) GREEN — regression pinned in vitest.
- setimmediate polyfill replacement GREEN — `new Function` count in SW chunk = 0.
- UAT harness 33/33 GREEN preserved (REVISION iter-2 — empirically pinned in Task 2 verify).
- Pre-checkpoint bundle Gate 2 polarity flipped (1 → 0).
</success_criteria>

<output>
After completion, create `.planning/phases/04-harden-clean-up-optional/04-02-SUMMARY.md` capturing:
- 2 new build-gate test files added (Task 1)
- vite.config.ts diff (1-line exclude addition)
- src/background/index.ts diff (11-line prelude)
- generate-icons.js → .cjs rename + any package.json/README updates
- deferred-items.md closure-flip block
- Pre-fix vs post-fix `grep -c 'new Function' dist/assets/index.ts-*.js` (1 → 0)
- vitest baseline before/after
- UAT harness GREEN preservation evidence (REVISION iter-2 — grep `UAT harness: 33/33 assertions passed` count = 1 from `<verify><automated>` output)
- RED→GREEN flip for the no-new-function test (Task 1 RED → Task 2 GREEN)
- Commit refs (Task 1 + Task 2)
</output>
</content>
