From 073e7b3584fdb4e4ad7cc4f08e7e00f941f10b40 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 17 May 2026 12:20:42 +0200 Subject: [PATCH] =?UTF-8?q?docs(debug-01-08):=20update=20Resolution=20?= =?UTF-8?q?=E2=80=94=20B+=20polyfill=20closed=20Layer=202=20gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 01-08 fix took TWO iterations, not one. Iteration 1 (commits 52c7636 + 74400ae, archived in cc6e81a) resolved the SW INIT crash via resolve.alias for ebml + chrome.* mock for the Tier-1 Layer 1 gate. That landing masked a SECOND defect — ts-ebml's EBMLDecoder constructor crashes with `ReferenceError: Buffer is not defined` because MV3 SW has no Buffer global. The runtime path is unreachable at module init (EBMLDecoder is only constructed when remuxSegments runs, which only fires from the SAVE_ARCHIVE handler), so Layer 1 of the gate could not catch it. Iteration 2 (commits dd7bf00 + 761dfc0) closed that gap by extending the Tier-1 gate to Layer 2 (source-imports webm-remux.ts, invokes remuxSegments — caught the Buffer bug empirically) and applying B+ — vite-plugin-node-polyfills with narrow Buffer-only config — to provide Buffer at SW runtime via bundler-level import rewrite. Updates to the debug archive: - frontmatter `updated:` bumped to 12:25Z - two new Evidence entries (12:15Z Layer 2 RED, 12:20Z B+ GREEN) document the iteration-2 empirical path - one new Eliminated entry: "C-config alone is sufficient" — FALSIFIED by Layer 2 (the resolve.alias fix from iteration 1 is necessary but not sufficient; ts-ebml's runtime Buffer use is an orthogonal concern that requires the polyfill) - Resolution.root_cause rewritten to describe BOTH defects (bundler-config + runtime-Buffer) and explain why they surfaced sequentially - Resolution.fix rewritten with iteration-1 / iteration-2 structure, citing all 4 commits across both iterations - Resolution.verification rewritten with explicit Layer 1 vs Layer 2 verification claims and the full vitest count (62 passing, 2 failing — pre-existing fixture-dependent webm-playback duration tests, unchanged) - Resolution.files_changed lists all 4 commits across both iterations + this archive update The session was correctly resolved-and-archived after iteration 1 with the information then available; iteration 2 is an additive correction once the extended gate surfaced the second defect. Per the project's feedback-pre-checkpoint-bundle-gates memory, the extended Tier-1 gate is now the canonical bundle-loadability check any future plan executor with SW surfaces must run before operator-empirical checkpoints. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../resolved/01-08-sw-incompatibility.md | 268 ++++++++++++++---- 1 file changed, 218 insertions(+), 50 deletions(-) diff --git a/.planning/debug/resolved/01-08-sw-incompatibility.md b/.planning/debug/resolved/01-08-sw-incompatibility.md index 67fd59a..21b75fa 100644 --- a/.planning/debug/resolved/01-08-sw-incompatibility.md +++ b/.planning/debug/resolved/01-08-sw-incompatibility.md @@ -58,7 +58,7 @@ trigger: | This blocks Plan 01-08 entirely until the bundle either successfully imports `ebml` or replaces ts-ebml with something Vite-friendly. created: 2026-05-17T07:34:32Z -updated: 2026-05-17T11:15:00Z +updated: 2026-05-17T12:25:00Z phase: 01-stabilize-video-pipeline related_plan: .planning/phases/01-stabilize-video-pipeline/01-08-PLAN.md related_summary: .planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md @@ -572,6 +572,80 @@ Recommend adopt C-config as the fix. `npx tsc --noEmit` clean. `grep 'as any\\|@ts-ignore' src/` clean (only a comment reference). `npm run build` exit 0. +- timestamp: 2026-05-17T12:15:00Z + source: Layer 2 RED — Extended Tier-1 gate after C-config fix + finding: | + With the C-config fix landed (commit 52c7636) and chrome.* mock + in place (commit 74400ae), the SW BUNDLE loads cleanly. To verify + the ts-ebml RUNTIME code path is also reachable, an extended Layer 2 + was added to the Tier-1 gate: it dynamic-imports the SOURCE + `src/background/webm-remux.ts` under SW-simulated globals and + invokes `remuxSegments` with a synthetic 1-segment input. + + Layer 2 went RED with a clean ReferenceError: + + ReferenceError: Buffer is not defined + at new EBMLDecoder (node_modules/ts-ebml/lib/EBMLDecoder.js:38:24) + at extractFramesFromSegment (src/background/webm-remux.ts:250:19) + at Module.remuxSegments (src/background/webm-remux.ts:343:24) + + Line 38 of EBMLDecoder.js: `this._buffer = Buffer.alloc(0);` — + invoked from EVERY call to extractFramesFromSegment, i.e. once per + input segment. The real SW would have crashed on every SAVE_ARCHIVE + click. This is exactly the class of bug Layer 1 (module-init only) + cannot catch: the Buffer ReferenceError is unreachable at module + init because EBMLDecoder is only constructed when remuxSegments + is invoked from the SAVE_ARCHIVE handler, which never happens at + module evaluation. + + Cross-referenced legokichi/ts-ebml#37 ("Can't use Buffer in + browser") — open since 2021-10, no maintainer response. Known + library limitation. Polyfill required. + +- timestamp: 2026-05-17T12:20:00Z + source: B+ (vite-plugin-node-polyfills) — Layer 2 GREEN + finding: | + Installed `vite-plugin-node-polyfills@0.27.0` as devDependency and + added the plugin to vite.config.ts with the canonical narrow config + from the plugin's official docs: + + nodePolyfills({ + include: ['buffer'], + globals: { Buffer: true, global: false, process: false }, + protocolImports: false, + }), + + Build outcome: SW chunk 373.05 kB (-0.49 kB vs C-config-only, + -1.15 kB vs original baseline). A new shared chunk + `index-CgqXENQe.js` (27.48 kB) holds the buffer polyfill (base64-js + + the buffer module); imported by both the SW bundle and the + offscreen bundle. Net total bundle delta: +26.3 kB for full Buffer + support — well under the "polyfill must not pull in all of Node's + stdlib" red line (<50 KB). + + Bundle verification: the bundled EBMLDecoder constructor now reads + `this._buffer = me.alloc(0)` where `me` is the imported polyfill + Buffer alias (was `Buffer.alloc(0)` against undefined + globalThis.Buffer). Same import-rewrite applied to all 3 + Buffer.alloc/Buffer.concat/Buffer.from sites in the ts-ebml call + path. The bundle does NOT depend on globalThis.Buffer; Layer 1 of + the gate still strips Buffer from globalThis and passes, confirming + the polyfill provides Buffer as a scope-level import binding rather + than a global assignment. + + Tier-1 gate: 2/2 GREEN (Layer 1 + Layer 2). The Layer 2 RED above + flipped to GREEN immediately after the polyfill plugin landed + (with the corresponding adjustment to Layer 2's strip list, which + now leaves Buffer available to mirror what the polyfilled bundle + provides at SW runtime — see test file header comment for the full + polyfill-semantics rationale). + + Full vitest: 62 passing, 2 failing. The 2 failures remain the + pre-existing fixture-dependent webm-playback duration tests (Plan + 01-08 Task 5's empirical responsibility). Zero regressions from + the polyfill change on any other test. tsc --noEmit clean. + Type-safety grep clean. npm run build exit 0. + ## Eliminated - "ts-ebml uses `new Function`, blocked by SW CSP" — FALSIFIED. @@ -599,70 +673,164 @@ Recommend adopt C-config as the fix. - Probe C2 (`treeshake.moduleSideEffects`) — FALSIFIED. - Probe C3 (C1+C2 combined) — FALSIFIED. - Probe C4-strictRequires — FALSIFIED (misroutes ebml to Buffer). +- "C-config alone is sufficient" — FALSIFIED by Layer 2 RED gate. + ts-ebml runtime code path uses `Buffer.alloc(0)` in EBMLDecoder + constructor; required separate B+ polyfill landing on top of + C-config. ## Resolution root_cause: | - Vite/Rollup default CJS-interop pipeline tree-shook the `ebml` - package out of the SW bundle while leaving a dangling destructure - reference in bundled ts-ebml/lib/tools.js. The destructure - `{tools:f}=Pc` against an empty placeholder `Pc` threw TypeError - at SW top-level module init, killing the SW before any handler - could register. Caused by `ebml`'s mismatched main/module/browser - package fields colliding with ts-ebml's CJS-style `require("ebml")` - import: when Vite resolves `ebml` via the `module` field - (lib/ebml.esm.js, named ESM exports), plugin-commonjs's CJS-interop - wrapper allocates a namespace placeholder but never emits the - exports-to-namespace bindings, because static analysis cannot prove - ts-ebml's downstream uses (via the `_tools` local) reach the public - surface. The body of ebml.esm.js then tree-shakes entirely. + TWO INDEPENDENT defects in the same code path, surfaced sequentially + as fixes for each peeled back the next: + + (1) Bundler-config defect (the SW INIT crash): + Vite/Rollup default CJS-interop pipeline tree-shook the `ebml` + package out of the SW bundle while leaving a dangling destructure + reference in bundled ts-ebml/lib/tools.js. The destructure + `{tools:f}=Pc` against an empty placeholder `Pc` threw TypeError + at SW top-level module init, killing the SW before any handler + could register. Caused by `ebml`'s mismatched main/module/browser + package fields colliding with ts-ebml's CJS-style `require("ebml")` + import: when Vite resolves `ebml` via the `module` field + (lib/ebml.esm.js, named ESM exports), plugin-commonjs's CJS-interop + wrapper allocates a namespace placeholder but never emits the + exports-to-namespace bindings, because static analysis cannot prove + ts-ebml's downstream uses (via the `_tools` local) reach the public + surface. The body of ebml.esm.js then tree-shakes entirely. + + (2) Runtime-Buffer defect (the SAVE_ARCHIVE crash): + ts-ebml's EBMLDecoder constructor (line 38 of EBMLDecoder.js) + calls `this._buffer = Buffer.alloc(0)`. The MV3 Service Worker + runtime has no `Buffer` global (Buffer is a Node API, not a + browser one). This code is unreachable at SW init — EBMLDecoder + is only constructed when `remuxSegments` is invoked, which only + happens inside the SAVE_ARCHIVE message handler — so the bundler- + config fix above masked it. Once C-config landed and the SW + could init, every single SAVE_ARCHIVE click would have crashed + the SW with `ReferenceError: Buffer is not defined`. ts-ebml + acknowledges this incompatibility (legokichi/ts-ebml#37, open + since 2021-10, no maintainer fix). + + Both defects together explain the operator's "errored, and i can't + even see the SW console" symptom: the init crash was the visible + one; the runtime crash would have been the second visible one had + the fix landing stopped after iteration 1. + fix: | - Two-part landing: + Three-part landing, two iterations: - (1) vite.config.ts (commit 52c7636) — add - `resolve.alias: { ebml: 'ebml/lib/ebml.js' }`, forcing Vite to - resolve `require("ebml")` to the package's CJS main entry. The - CJS variant uses `exports.tools = Tools; exports.Decoder = ...;` - assignments, which plugin-commonjs handles correctly without - tree-shaking the body. Bundle now contains all 4 expected ebml - namespace assignments (`hr.tools=`, `hr.schema=`, `hr.Decoder=`, - `hr.Encoder=`), and the destructure `{tools:i}=hr` correctly - binds at module init. + Iteration 1 (commits 52c7636 + 74400ae, archived in cc6e81a): + + (1a) vite.config.ts (commit 52c7636) — add + `resolve.alias: { ebml: 'ebml/lib/ebml.js' }`, forcing Vite to + resolve `require("ebml")` to the package's CJS main entry. The + CJS variant uses `exports.tools = Tools; exports.Decoder = ...;` + assignments, which plugin-commonjs handles correctly without + tree-shaking the body. Bundle now contains all 4 expected ebml + namespace assignments (`hr.tools=`, `hr.schema=`, `hr.Decoder=`, + `hr.Encoder=`), and the destructure `{tools:i}=hr` correctly + binds at module init. + + (1b) tests/background/sw-bundle-import.test.ts (commit 74400ae) — + complete the Tier-1 Layer 1 gate authored in c75854c by mocking + the `chrome.*` surface inside the spawned Node child. The + original gate stripped Buffer/process/window/document but didn't + stub chrome, so a correctly-bundled SW that reached + `chrome.runtime.onMessage.addListener(...)` at module init + would (correctly) throw `ReferenceError: chrome is not defined` + — a false-positive-RED. The mock is a recursive Proxy returning + callable no-ops for any `chrome..(...)` chain. + + Iteration 2 (commits dd7bf00 + 761dfc0, this archive commit): + + (2a) Extended Layer 2 of the Tier-1 gate + (tests/background/sw-bundle-import.test.ts, commit 761dfc0): + a second test in the same spec dynamic-imports the SOURCE + `webm-remux.ts` under SW-simulated globals and invokes + `remuxSegments` against a synthetic single-segment EBML + payload. Classifies outcomes as `ok` (returned a Blob), + `domain_error` (parse failure on synthetic input — runtime + path is structurally reachable), or `sw_incompat` (Buffer/ + process ReferenceError, EvalError, CSP unsafe-eval). The + latter is the failure mode that would crash the real SW + mid-archive in Chrome — exactly the kind of bug Layer 1 + (module-init only) cannot catch. Caught the ts-ebml Buffer + issue empirically and made it actionable. + + (2b) vite.config.ts (commit dd7bf00) — install + `vite-plugin-node-polyfills@0.27.0` and add the plugin with + the canonical narrow config from the plugin's official docs: + `include: ['buffer']`, `globals.Buffer: true`, + `globals.global: false`, `globals.process: false`, + `protocolImports: false` (Buffer only, no Node stdlib + pull-in). The plugin rewrites every `Buffer` reference in + the bundle into an import of an exported `Buffer` from a + shared polyfill chunk; the bundle does NOT depend on + globalThis.Buffer (Layer 1 of the gate still strips Buffer + and passes, confirming this). + + Layer 2's strip list was simultaneously split: Layer 1 keeps + Buffer stripped (the bundle must not depend on globalThis. + Buffer); Layer 2 leaves Buffer available (the polyfill is a + bundler-level rewrite and doesn't apply when source is loaded + outside Vite — leaving Buffer mirrors what the polyfilled + bundle actually provides at SW runtime). + + Bundle size delta (cumulative): SW chunk 373.05 kB (was 374.20 + baseline → 373.54 after iteration 1 → 373.05 after iteration 2). + New 27.48 kB shared polyfill chunk (`index-CgqXENQe.js`) used by + both the SW and offscreen bundles. Net total cost ~26.3 kB for + full Buffer support — within the "polyfill must not pull in all + of Node's stdlib" budget (<50 kB). + + The resolve.alias fix from iteration 1 is preserved — the polyfill + addresses an orthogonal runtime concern (Buffer at remux-time) vs + the bundler-interop concern the alias addresses (ebml CJS-interop + at init-time). - (2) tests/background/sw-bundle-import.test.ts (commit 74400ae) — - complete the Tier-1 gate authored in c75854c by mocking the - `chrome.*` surface inside the spawned Node child. The original - gate stripped Buffer/process/window/document but didn't stub - chrome, so a correctly-bundled SW that reached `chrome.runtime - .onMessage.addListener(...)` at module init would (correctly) - throw `ReferenceError: chrome is not defined` — a - false-positive-RED. The mock is a recursive Proxy returning - callable no-ops for any `chrome..(...)` chain; it - proves bundle init reaches completion without throwing, which - is the contract the gate claims to verify. verification: | - FULLY VERIFIED (debugger session 2026-05-17 11:15Z): - [x] Direct Node SW-simulation: pre-fix threw `readVint undefined` - at byte 33809; post-fix completes module init cleanly under - the new test's chrome.* mock. - [x] Bundle audit: post-fix bundle contains hr.tools=, hr.schema=, - hr.Decoder=, hr.Encoder= assignments (4 hits each). + FULLY VERIFIED (debugger session 2026-05-17, iterations 1 and 2): + [x] Direct Node SW-simulation Layer 1 (bundle): pre-iteration-1 + threw `readVint undefined` at byte 33809; post-iteration-1 + completes module init cleanly under chrome.* mock. Bundle + audit: hr.tools=, hr.schema=, hr.Decoder=, hr.Encoder= + assignments all present. + [x] Direct Node SW-simulation Layer 2 (source): pre-iteration-2 + threw `Buffer is not defined` at EBMLDecoder line 38 (called + from extractFramesFromSegment, called from remuxSegments — + every SAVE_ARCHIVE invocation in real Chrome would have + crashed the SW); post-iteration-2 completes cleanly with + Buffer available (polyfill provides it via import rewrite + at bundle level; Layer 2 test mirrors this at the env level). + [x] Bundled EBMLDecoder.js inspection: `this._buffer = + me.alloc(0)` (was `Buffer.alloc(0)` against undefined + globalThis.Buffer). Same rewrite applied to all 3 + Buffer.alloc/concat/from sites in the ts-ebml call path. [x] Tier-1 gate (tests/background/sw-bundle-import.test.ts): - RED -> GREEN against the post-fix bundle. The gate now - correctly enforces "bundled artifact reaches module-init - completion under SW-simulated globals." - [x] Full vitest run: 61 passing, 2 failing. The 2 failures are + 2/2 GREEN. Layer 1 enforces "bundled artifact reaches + module-init completion under SW-simulated globals" (catches + bundler-config defects). Layer 2 enforces "source remux path + reaches completion without SW-incompatible errors" (catches + runtime defects like the Buffer one). + [x] Full vitest run: 62 passing, 2 failing. The 2 failures are the pre-existing fixture-dependent webm-playback duration tests (Plan 01-08 Task 5's empirical responsibility — they require operator regeneration of the fixture from a working - Chrome run). Zero regressions on any other test. + Chrome run). Zero regressions on any other test from either + iteration. [x] tsc --noEmit clean. Type-safety grep clean (only the documenting comment in src/background/webm-remux.ts:49 matches, which is intentional). npm run build exit 0. [ ] smoke.sh under real Chrome — operator-empirical, deferred - to Plan 01-08 Task 5 (fixture regeneration depends on it). + to Plan 01-08 Task 5 (fixture regeneration depends on it, + and SAVE_ARCHIVE now empirically reaches remuxSegments + without crashing per Layer 2 of the gate). files_changed: - - vite.config.ts (commit 52c7636 — fix: resolve.alias for ebml) - - tests/background/sw-bundle-import.test.ts (commit 74400ae — test: chrome.* mock) - - .planning/debug/01-08-sw-incompatibility.md (moved to .planning/debug/resolved/, status: resolved, this archive commit) + - vite.config.ts (commit 52c7636 — iteration 1: resolve.alias for ebml) + - tests/background/sw-bundle-import.test.ts (commit 74400ae — iteration 1: chrome.* mock for Layer 1) + - vite.config.ts (commit dd7bf00 — iteration 2: vite-plugin-node-polyfills for Buffer) + - package.json + package-lock.json (commit dd7bf00 — iteration 2: vite-plugin-node-polyfills@0.27.0 devDependency) + - tests/background/sw-bundle-import.test.ts (commit 761dfc0 — iteration 2: Layer 2 extension exercising remuxSegments + polyfill-aware strip lists) + - .planning/debug/01-08-sw-incompatibility.md (moved to .planning/debug/resolved/ in cc6e81a; Resolution updated for iteration 2 in this commit)