docs(debug-01-08): update Resolution — B+ polyfill closed Layer 2 gap
The 01-08 fix took TWO iterations, not one. Iteration 1 (commits52c7636+74400ae, archived incc6e81a) 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 (commitsdd7bf00+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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.<api>.<method>(...)` 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.<api>.<method>(...)` 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)
|
||||
|
||||
Reference in New Issue
Block a user