Repository navigation
Epic: stub elimination — every registered API is real or fails loud #4919
Description
Epic: every registered API is real, or it fails loud
Full-tree stub audit (2026-06-10, runtime + stdlib + api-manifest, verified against current main). A "stub" here means: an API that is registered and callable by name but returns a no-op, a hardcoded/fake value, or silently wrong behavior. For a native compiler, "compiles fine, runs wrong" is the worst failure mode — this epic exists so that by v1.0 no API can lie. (Spun out of the stable-release/versioning discussion; this is a 0.8-line "honest surface" gate.)
Policy (the actual deliverable)
Every API is in exactly one of three states, machine-readable in the manifest and enforced by CI:
- Real — behaves like Node.
- Partial — documented subset; out-of-subset input throws, never silently degrades.
- Stub — flagged
stub: true, emitsperry_stub_warnon first call, visible in.d.ts/reference docs, throws underPERRY_STRICT_STUBS=1.
Keystone issue: #4918 (manifest stub flag is never set; two stub registries don't know about each other).
Stub inventory — sub-issues
| Issue | Cluster | Severity |
|---|---|---|
| #4911 | node:dns + node:dgram loopback-only fakes |
SILENT-LIE |
| #4912 | child_process.spawn() returns null; exec() secretly synchronous |
LOUD + SILENT-LIE |
| #4913 | Atomics.wait/notify/waitAsync non-blocking fakes (post-#4794) |
SILENT-LIE |
| #4914 | node:cluster — no socket/listening-handle distribution |
SILENT-LIE |
| #4915 | Web Streams BYOB readers + ByteLengthQueuingStrategy throw (post-#1545) |
LOUD |
| #4916 | v8 heap snapshot = empty-but-valid graph; inspector/repl look real | SILENT-LIE |
| #4917 | stdlib adapters: ignored options + success-returning no-ops (zlib/http.Agent/mongodb/mysql2/pg/worker_threads/fastify/backoff) | mixed |
| #4918 | Keystone: manifest stub: true population + CI cross-check + strict mode |
infra |
Pre-existing open issues that are stub-class (tracked there, listed for completeness)
- globals: make WeakRef and FinalizationRegistry actually weak #2656 —
WeakRef/FinalizationRegistrynot actually weak ("API exists, semantics lie") - runtime: expose Atomics operations for SharedArrayBuffer views #2876 — Atomics ops on SAB views (interacts with Atomics.wait/notify/waitAsync are non-blocking fakes — real blocking + cross-agent wakeups (follow-up to #4794) #4913)
- Compile a standard Express app: full blocker inventory (codegen emits undefined @perry_closure_* globals → linked-but-crashing binary) #3527 — codegen emits undefined
@perry_closure_*globals → linked-but-crashing binary (the linker-level analog of a stub: fail loud at link time)
Found-but-not-filed (intentional / by-design, no action unless policy changes)
stdlib_stubs.rslink-time fallbacks (WebSocket/fetch/readline when perry-stdlib isn't linked) — legitimate, but should all route throughperry_stub_warn(folded into api-manifest: stub flag exists but is never set — populate it, cross-check with stub_diag, and surface in docs + a strict mode #4918).ui_harmonyos_stubs.rs— platform-gated zero-returning UI symbols (perry/ui FFI symbols undefined on --target harmonyos (perry_ui_embed_nsview, others) — block on .so load #395); exactly what the manifeststubfield was invented for.node:vmSourceTextModule — throwsERR_PERRY_VM_UNIMPLEMENTEDwith issue pointer; this is the model behavior the rest should copy.node:stream_read()throwing "not implemented" — correct Node semantics, not a stub.
Audit notes (claims checked and dropped)
- async context (
AsyncLocalStorage/async_hooks) — real since AsyncLocalStorage: real async-context tracking across await / microtasks / timers #788/async_hooks: real createHook lifecycle + asyncId tracking #789 closed (May 16); CLAUDE.md's "name-only stubs" note is stale and should be updated. node:tty"always reports non-TTY" — not confirmed; tty raw-mode work (TUI gap: readline + tty.setRawMode + raw-mode stdin reader #347/process.stdin: support listener removal and lifecycle detach for TUI teardown #3962) landed for the ink push.node:netloopback-only — not confirmed on current main; dropped from node:dns + node:dgram are loopback-only fakes — implement real network I/O behind a deterministic-mode flag #4911.
Done when
All eight sub-issues closed (or individually re-triaged with rationale here), and the #4918 CI gate is the thing preventing recurrence — not this list.
Activity
feat(stubs): stub-elimination keystone — populate manifest stub flag,…
feat(stubs): stub-elimination keystone — populate manifest stub flag,…
proggeramlug commentedon Jun 10, 2026
Keystone landed — stub surface is now honest (PR #4929, merged
a389f9cdf)The #4918 keystone is complete and merged, which is the epic's "Done when" recurrence-prevention gate:
ApiEntry.stubis now actually populated (it existed but was set on 0 of ~2,500 entries). Added const.stub()/.stub_note(reason)builders + astub_notefield (distinguishes a pure no-op from a real-but-partial API without an enum migration)..d.ts(@perryStub <note>),reference.md(⚠ column), and--print-api-manifestJSON surface them automatically.crates/perry-api-manifest/tests/stub_inventory.rspins the exact inventory, requires every stub to carry an issue-tagged reason, and allowlists which modules may contain stubs — so a new silent lie can't ship without a reviewed change.perry_stub_warnand, underPERRY_STRICT_STUBS=1, throwERR_PERRY_UNIMPLEMENTEDinstead of returning a fake.Per-sub-issue status
PERRY_DETERMINISTIC_NET.spawnis real since #1780 (verified). Premise was stale; left unflagged. Open only forexec()same-tick-callback timing.timeout=0polls +not-equaluntouched). Open for real shared-SAB + futex wakeups.ByteLengthQueuingStrategy.Also done
AsyncLocalStorage/async_hooks"name-only stubs" note in CLAUDE.md (real since AsyncLocalStorage: real async-context tracking across await / microtasks / timers #788/async_hooks: real createHook lifecycle + asyncId tracking #789, per this epic's audit).The seven implementation issues stay open for their real halves (multi-stage by their own framing); the manifest now makes them visibly fake rather than silently fake, which is the floor this epic set. Closing the keystone #4918.
proggeramlug commentedon Jun 11, 2026
Epic complete — closing
All eight sub-issues are now closed:
stubflag populated (100 entries), first-callperry_stub_warn,PERRY_STRICT_STUBS=1throws, CI drift guardstub_inventory.rs— PR #4929PERRY_DETERMINISTIC_NET=1keeps the deterministic modespawnverified real (stale premise);exec/execFilerouted through the async spawn reactor — PR #4941wait/notify/waitAsync(global SAB backing + futex table) — PR #4952ByteLengthQueuingStrategy— PR #4966The recurrence-prevention gate is in place and is the thing enforcing the policy going forward:
crates/perry-api-manifest/tests/stub_inventory.rspins the exact stub inventory in CI, requires an issue-tagged reason per entry, and allowlists which modules may contain stubs — a new silent lie can't ship without a reviewed change. Stub state is visible in.d.ts(@perryStub),reference.md, and--print-api-manifest, and flagged paths warn on first call / throw underPERRY_STRICT_STUBS=1.Pre-existing stub-class issues remain tracked separately: #2656 (WeakRef/FinalizationRegistry), #2876 (Atomics on SAB views), #3527 (undefined
@perry_closure_*globals).