compat: W43 five-hour Node/Bun corpus sprint - #81
Conversation
- #80 Define the audited runnable-corpus denominator, five-hour quantitative gates, source-driven three-stage workflow, bounded parallel ownership, and PR evidence contract. Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
- #80 Define exact baseline gates, Wave A worklists, rolling concurrency, integration checks, full verification, and PR milestones. Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
- #80 Record same-binary Node and Bun full-corpus categories, provenance, and the audited runnable gap before source implementation. Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
Checkpoint 0 — fresh full baseline, before implementation
Node full corpus
Raw Bun full corpus
Raw Combined audited runnable: Wave A fixed lane targets: A1 Node zlib/Buffer +3–5; A2 Node assert +2–4; A3 Node permission +2–4; A4 Bun N-API +3–5; A5 Bun test runner +3–5. Five logical lanes run in rolling batches on three physical worker slots. No W43 source implementation has started. Current CI state for this checkpoint: pending/not yet audited. The final 100% target remains open; the first sprint target is net +30 to +45 with zero stable-green regressions and no timeout/OOM increase. Build provenance note: the first build was rejected because a worktree-local installed JSC package cached GCC15 |
- #80 Record five disjoint baseline-confirmed lists, fixed targets, struck checks, and source ownership before worker dispatch. Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
- #82 - #80 Remove the incomplete partial-deep implementation and discard unsupported skipPrototype state. Preserve instance receivers, and only attach diff metadata to AssertionError instances. Expand the JSC runtime probes for retained behavior. Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
Move permission translation behind the public fs validators and preserve pinned Node's immediate callback rejection order for access and ownership calls. Add a real JSC runtime regression covering invalid arguments, path-like getters, denial timing, and the synchronous-denial APIs.\n\n- #84\n- #80 Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
Invoke immediate permission-denial callbacks for their side effects while preserving the wrapped fs API return value. Extend the runtime regression to require undefined from access, chown, and lchown when callbacks return a value. Refs #84 and #80. Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
Replace the one-shot due-only timer drain with the runtime's bounded event-loop pump. This waits for the host timer's real 1ms deadline without an arbitrary sleep and makes the composite callback assertion deterministic.\n\nRefs: #85\nRefs: #80\n\nSigned-off-by: Sunrisepeak <speakshen@163.com>\nCo-authored-by: Codex (GPT-5) <>
W43 interim checkpoint — Wave A batch 1 composedHead: Real focused results against the frozen baseline:
Coordinator gates:
The initial full JSC gate exposed a pre-existing one-millisecond async-hooks test race. Issue #85 replaced its one-shot due-only timer drain with the existing bounded event-loop pump; measured exact test is now 10/10 without weakening assertions. This is focused batch-1 evidence, not a post-Wave-A full Node/Bun corpus result. A4 N-API finalizer (+3 target) and A5 test-runner settlement/mock (+3 combined target, recorded as two distinct mechanisms) are now running in parallel from the frozen composed binary. |
- #88 - #80 Retry late mock cache lookups through a canonical package path without loading unknown modules, then fan replacements through the existing named-import subscriptions. Add a symlinked-package member regression. Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
- #80 Record the reviewed five-lane same-binary +13 focused result, serial and impact gates, remaining reds, and scope boundary before Wave B. Signed-off-by: Sunrisepeak <speakshen@163.com> Co-authored-by: Codex (GPT-5) <>
`async function f(){}` in statement position is an AsyncFunctionDeclaration,
not an expression statement. parse_statement_ had no case for it, so it fell
through to the expression path and kept parsing postfix: a following
statement-leading `[` became a computed member access, which then rejected the
legal trailing comma in `[a, b,]` with "Unexpected ]". That took
test-runner-global-setup-teardown.mjs down before a single test ran.
node keeps four builtins requirable only WITH the `node:` scheme
(lib/internal/bootstrap/realm.js schemelessBlockList): test, test/reporters,
sqlite, quic. mbun registered each under both keys, so a bare
`require("test")" returned the builtin runner and a userland
node_modules/test could never shadow it. builtin_module now misses on the bare
form so it falls through to the ordinary resolver; the `node:` spec is a
different string and is unaffected. The BuiltinModule shim in
node_process_extra already modelled the same list for module.isBuiltin.
The four --expose_externalize_string globals were installed unconditionally as lazy accessors returning undefined without the flag. Their PRESENCE is the observable, not their value: the REPL completes globals from ObjectGetOwnPropertyNames(globalThis) (node repl.js filteredOwnPropertyNames keeps every identifier-shaped own name, enumerable or not), so a flagless process offered `isOneByteString` for the input `I`. Greens test-repl-tab-complete.js.
__mbun_drain_timers() gated firing on `T.fired < 200000` with T.fired only ever incremented, so the 200,000th timer in a process was the last one to fire: the queue stopped draining, the loop went idle, and the process exited 0 with no output. setTimeout's clear/refresh/repeat leak fixtures schedule 115 * 2_000 = 230_000 timers and died silently ~85% through the measurement loop, which is why all three scored as an empty stdout. Reset the counter whenever a drain finishes without exhausting its per-call budget: everything due was fired, so the workload is not running away. Only an unbroken run of saturated drains -- a callback that keeps re-queueing due-now work -- can still reach the cap.
fetch.preconnect was a no-op stub, so all 12 tests in test/js/web/fetch/fetch-preconnect.test.ts failed -- 11 waiting forever for a connection that was never dialed, and one because the URL validation never ran. Dial the origin and park the socket in the same keepalive hive doFetch() checks out from, so the first request to that origin reuses it. Validation order and messages follow Bun__fetchPreconnect (webcore/fetch.rs:228): WHATWG parse -> scheme -> hostname -> port. Only http:// is dialed; poolTake() hands a `tls` entry to the request writer as if the handshake had completed, so an https preconnect would poison the hive and is left as validate-then-no-op. --fetch-preconnect <URL> is now parsed (repeatable, both spellings) into a runtime list exposed as __mbunHttpNative.preconnectUrls(); the net JS image dials each before the entry module runs, mirroring run_command.rs do_preconnect.
node reads its main module from stdin when there is no file to run: an explicit `-` operand, or a bare invocation whose stdin is not a terminal (a terminal is the REPL instead). mbun printed its help text for the first form and "Script not found \"-\"" for the second. `-` is not an option, so it keeps its argv slot -- `mbun - --opt` puts --opt at process.argv[2]. Separately, node's --print is a BOOLEAN option, separate from --eval's string, so `-p -e 42` is one eval whose result is printed. mbun took args[1] as the source unconditionally, so `mbun -p -e 42` evaluated the token "-e" and died with "ReferenceError: e is not defined". Greens test-stdin-script-child.js, test-stdin-script-child-option.js.
node marks every option kAllowedInEnvvar or not (src/node_options.cc).
The ones it withholds decide WHAT the process runs rather than how --
printing a version or help text, an eval string, the REPL, a syntax
check, the test runner, or the `--` terminator. Meeting one in
NODE_OPTIONS is fatal before any JS runs, with
"<argv0>: <opt> is not allowed in NODE_OPTIONS" and exit 9.
mbun ignored the environment entirely here. That is now observable
rather than merely wrong: with node's stdin-main-script behaviour in
place, `execFile(execPath, {env: {NODE_OPTIONS: '--eval'}})` no longer
prints help and exits -- it blocks on a stdin pipe the parent never
closes.
Implemented as node's DENY set, not the complement of its allow set:
mbun accepts node flags it has not modelled everywhere else, so an
allowlist here would turn every uncatalogued flag into a hard startup
failure -- a much larger claim than the one node is making.
Greens test-cli-node-options-disallowed.js.
node reports a failed --check through its ordinary uncaught-exception printer (lib/internal/main/check_syntax.js re-throws the parse error), which is a five-part frame: `<file>:<line>`, the offending source line, a caret, a blank line, then the error on a line of its own. mbun put the filename and the error on ONE line, so /^SyntaxError: Unexpected identifier\b/m never matched. Greens test-cli-syntax-piped-bad.js.
node lib/internal/util/trace_sigint.js arms a PROCESS-wide SIGINT watchdog, so a worker is refused with ERR_WORKER_UNSUPPORTED_OPERATION. It was absent entirely, so the call died as "not a function" with no .code. Defined only in the worker branch, following process.execve above: on the main thread mbun has no watchdog to arm, and an absent property is the honest report of that. Greens test-trace-sigint-in-worker.js.
Two categories that produced nothing. Both are sited where node sites them, not wrapped from outside the module -- the moments node marks (a ServerResponse being constructed, a connect request completing) are not reachable from the trace agent. node.net.native: one nestable-async `connect` span per connection attempt, opened where node queues the uv connect request and closed when the attempt resolves. The pipe form carries node's path_type / pipe_path pair (an abstract socket is the leading-NUL name with the NUL dropped), the TCP form ip / port, and the end the status. The end is bound to 'connect'/'close' rather than to 'error': adding an 'error' listener would suppress the throw an unhandled socket error owes the program. node.http: http.server.request from the ServerResponse constructor to _finish, http.client.request from the ClientRequest constructor to the parsed response head -- node's own four sites. The agent gains emitGroup/emitGroupArgs for this: a compound-category test plus a recorder that writes the compound string verbatim, with the TRACE_EVENT_*1/*2 form putting its key/value pairs directly in `args` rather than under `args.data`. Greens test-trace-events-net.js, test-trace-events-net-abstract-socket.js, test-trace-events-http.js.
node writes `(node:<pid>, thread:<tid>)` when the environment that exited is a worker's and `(node:<pid>)` when it is the main thread's; mbun always wrote the latter, so the worker half of the test matched zero thread ids. mbun runs a worker as a child process, so the tid comes from worker_threads rather than from a thread id this process could ask the OS for. Greens test-trace-exit.js.
js_parser lowers `import(x)` to `__mbun_dyn_import(require, x)`, capturing
the module's require BY NAME. A CJS module that declares its own `require`
in an enclosing scope therefore hands the helper that binding instead of the
wrapper parameter -- and emscripten's node preamble does exactly this:
if (ENVIRONMENT_IS_NODE) {
let { createRequire: e } = await import("module"); ... var require = e(t);
}
`var` hoists the shadow over the whole function, so the name is still
undefined when the lowered import runs, and every emscripten-built package
died with "req is not a function (In 'req(requested, attr, 1)')" the moment
it initialised -- @electric-sql/pglite and pg-gateway both.
Fall back to a cwd-rooted require when the captured binding is not callable,
the same shape builtins/s3.cppm already uses. The branch is only reachable
where the call previously threw outright, so it is strictly additive: bare
specifiers (all the shadowing preamble ever asks for -- "module", "fs",
"path") resolve identically.
Both files advance past this blocker to an independent emscripten WASM-init
stall, so neither turns green yet; corpus green is unchanged at 44/59.
Gate: js/bun/resolve + js/bun/module 36 files identical before/after
(19 green, 207 pass / 76 fail, 48264 expects); third_party reachable set
44 green before and after.
…led GET
bun splits the manifest/tarball fetch failure exactly on whether the edge is
required: add_error_pretty for a required one, add_warning_pretty for an
optional one, both over the same "GET {url} - {status}" body
(PackageManager/runTasks.rs:485-503 and :835-852). mbun's fail_dep_ returned
early for every optional failure, so an optional dependency whose tarball 404s
installed silently.
Adds a fail_fetch_ seam used only at the three fetch-settle sites (manifest
failure, tarball request construction, tarball response), carries the messages
out on registry_install::Summary::warnings -> InstallSummary::warnings, and
lets the CLI print them as 'warn: ...' on stderr. Every other optional failure
-- os/cpu mismatch, no matching version, an unsafe name -- stays silent, which
is why this is a separate seam rather than a flag on fail_dep_.
cli/install/bun-install-retry.test.ts 7 pass/3 fail -> 9 pass/1 fail.
…lapsed time
bun's save_lockfile prints 'Saved lockfile' on stderr for every subcommand that
writes one; only 'mbun install' echoed it, so a successful 'mbun add' wrote
bun.lock silently. Two further gaps in the same printer: the 'installed' line
reported the RANGE written into package.json ('installed BaR@^0.0.2') where bun
reports the concrete version that landed in node_modules ('installed
BaR@0.0.2'), and the summary line carried no ' [N.NNms]' suffix -- which the
corpus strips with /\s*\[[0-9.]+m?s\]\s*$/, a regex that only matches when the
suffix is present, so its absence left a spurious trailing line.
cli/install/bun-install-retry.test.ts 'retries on 500' now reaches its
node_modules/.cache assertion (69 -> 76 expect() calls evaluated in the file).
…hared /tmp gate lists W48's cli/install brief claimed the group had never had a lane; struck.tsv records wave 65 measuring all 73 files, landing a 200-line parser, gaining +0, and writing 'do not re-run this vein expecting files'. The W48 lane reproduced that conclusion at the cost of its budget. Separately, a lane reported that the corpus runner mutates its --list file. It does not; the runner writes only inside --out. Five concurrent lanes sharing bare /tmp paths for gate lists is what turned a 36-line list into 1301 lines. Recording the correction so future lanes do not defend against a bug that does not exist.
11 manifest files in 7.5 lane-hours against a 37-file target; composed delta is +20 because one lane's fix carried 10 collateral greens its manifest could not show. Two of the five lanes returned 0 for reasons that were my briefing errors, both recorded in the rows so the next planner inherits them.
W48 verdict — +20 measured, and three latent defects worth more than the countFrozen binary Lane result
11 manifest files in 7.5 lane-hours; goal accuracy 0.30. Composed measurement is higher than the manifest sum because C4's fix carried collateral: node +18, bun +2 = +20 this wave. Total progress
Cumulative regressions across all five waves: 1 ( Estimate to 100%1,373 runnable files remain. At the five-wave mean of ~23 files/wave, that is ~60 waves / ~190 coordinated wall-hours for what is reachable. The architecture-gated remainder is unchanged and now fully priced: QUIC 237 (60-100 lane-hours for the floor, 200-300 for all of it), inspector+debugger 132 (8-14 engineer-weeks, and no per-file gate so no partial credit exists), workers-as-processes ~10, JSC readonly-assign wording 4, the prebuilt's dead execution-time-limit callback 4, Realistic landing on today's architecture stays 96-98% of runnable. Three latent defects, none of which a file count would have surfacedA silent truncation in the timer loop. A parser correctness bug. Stack strings are not node-compatible for external code. Also filed: #100 — Two coordinator failures, both from not consulting the record
A third correction went the other way: a lane reported that the corpus runner mutates its |
The file is zero bytes and referenced by nothing -- a stray artifact from the W48 repl/runner lane. GCC tolerated it; clang globs *.cppm as module interface units and rejects it outright: modules/jsc/src/js_engine.cppm:1:1: error: missing 'export module' declaration in module interface unit which is why the gcc lane went green while the llvm lane stayed red on the same head.
Both corpora re-measured in full on ONE frozen binary (f65f960b7ac7d212) on 2026-08-03, rather than carried forward as accounting: Node 3,267 / 4,433 (73.7%), runnable 3,267 / 3,919 (83.4%) Bun 1,076 / 1,902 (56.6%), runnable 1,076 / 1,806 (59.6%) Both 4,343 / 6,335 (68.6%), runnable 4,343 / 5,725 (75.9%) The coverage line is dropped from the title -- a number in an H1 goes stale the moment anything lands, and the table below is the place that carries its own measurement date and binary. Both READMEs were behind by different amounts (the Chinese one still read 3,065 / 934), so both are updated from the same run. The runnable denominators GREW this round -- Node 3,898 to 3,919 -- because 21 files that used to skip themselves now run; that is recorded explicitly so the wider denominator is not mistaken for a softer one. The self-skip breakdown is regenerated by grouping this run's own logs instead of carrying the previous table forward, and the two large blocks are priced: QUIC needs no protocol work (node builds on vendored ngtcp2 and the linked OpenSSL 3.5.1 exports everything that backend calls) but does need node's ~19k -line binding against JSC; the inspector block has no per-file gate, so no partial slice can score and flipping the flag early converts skips into failures. Elysia keeps its row with its own measurement date -- its node_modules is not in this checkout, so it could not be re-run and is not presented as if it had been.
Final same-binary full measurement — both corpora, and CI is greenFrozen binary
Node categories: pass 3,267; fail 565; skipped 514; timeout 85; OOM 2. Against the W43 baseline this PR opened on — Node 3,138, Bun 1,041 — that is Node +129, Two things worth stating precisely:
CI
The llvm and macOS lanes had been red on the same head where gcc was green, and it was one cause: Deleted. I also swept the tree for the same hazard — no other empty tracked file exists — so this READMEThe coverage line is out of the H1 in The Elysia row keeps its own 2026-07-30 date and is labelled as not re-run: its |
Merge record — protected surfaces, maintainer instruction, what was bypassedMerging on the maintainer's explicit instruction. Recording the governance trail here because This PR changes five protected surfaces, which §5 defines as a constitutional change that an agent may not self-merge without maintainer sign-off:
What was bypassed: the base branch policy requires a review, and a normal What was NOT bypassed:
205 commits, 340 files. Against the W43 baseline this PR opened on: Node +129, Bun +35. |
Goal
Run the first measured five-hour W43 sprint toward 100% of the audited runnable Node and Bun native corpora. This PR tracks issue #80 and implements the approved design and execution plan.
The sprint target is net +30 to +45 fully green files: Node +22 to +33 and Bun +8 to +12. The final 100% goal remains open until every audited runnable file is green.
Fresh same-binary baseline
Base:
163cb6d3fedb9d22cf069cb5f8e20fcb2bc76049Baseline source head:
0cfcd5915947c0f709e482165e96aef9531bc337Frozen binary SHA-256:
5e17080bb82a6c5ac46c56c8c66070409371e46e368bdf285304f42695f8d395test/paralleltest/**Node categories: pass 3,138; fail 660; skipped 535; timeout 97; OOM 3.
Bun categories: green 1,041; test-failure 704; all-skipped 73; timeout 44; blocked-external 19; no-tests 6; ahead-of-reference 4; load-error 4; OOM 4; crash 3.
Reproduction:
Exclusions are not passes. The runnable denominators are floors and cannot shrink through new skips.
Execution model
compat/is read-only; no assertion weakening.Current state
The baseline build initially exposed a worktree-local package-cache mismatch: the installed JSC package contained GCC 15.1
libstdc++.awhile the pinned compiler was GCC 16.1. The generated dependency copy was refreshed from the recipe-required GCC 16.1 archive; no repository source or corpus input changed, and this recovery is not counted as compatibility progress.