Repository navigation
ineffective-dynamic-import-ledger's closeBundle counter-probe replaces the real error whenever an earlier console plugin fails the build #6093
Description
Found while reproducing #5996 (the eager-closure ENOENT). Not caused by that card and not fixed there — filing so triage can rank it.
What I measured
scripts/vite-ineffective-dynamic-imports.ts ends with a counter-probe in closeBundle: if none of the 43 pinned INEFFECTIVE_DYNAMIC_IMPORT warnings fired, it errors. The probe is well-motivated — its own message says so: "or this build never got far enough to emit them, in which case the number to distrust is the zero, not the list."
The problem is what happens to the other error. closeBundle runs even when the build has already failed, and the error it throws supersedes the aggregated build error in vite build's output. The real cause is not appended, not printed above, not present anywhere in the log — it is gone.
Measured on apps/console, vite@8.2.1 + rolldown@1.2.3, base 7c96c9420, by making emit-eager-closure-report's writeBundle fail (an out-of-tree OBJECTSTACK_CLIENT_DIST override whose own dependencies do not resolve — see the sibling card).
With the full plugin list — the only thing the developer sees:
error during build:
Error [RolldownError]: 43 pinned ineffective dynamic import(s) did NOT fire. Either they were fixed
— delete them from `DEFEATED_LAZY_FIELD_WIDGETS` in `scripts/vite-ineffective-dynamic-imports.ts`
and record the win in the PR — or this build never got far enough to emit them, in which case the
number to distrust is the zero, not the list.
- packages/fields/src/widgets/AddressField.tsx
… 42 more lines …
grep -c ENOENT on that log: 0. The actual failure is invisible.
Same build, same override, that one plugin removed from the array:
error during build:
Build failed with 1 error:
[plugin emit-eager-closure-report]
Error: ENOENT: no such file or directory, open '.../apps/console/dist/@objectstack/core/logger'
Why it is worth a line
This is generic, not specific to the plugin I happened to be debugging: any console build that dies before the pinned warnings are emitted reports the ledger's counter-probe instead of its own cause, and hands the developer a 45-line list of field widgets that have nothing to do with the failure. The 43 did NOT fire message reads as a ledger drift, so the first instinct is to go edit DEFEATED_LAZY_FIELD_WIDGETS — which is exactly the wrong move and would break the ledger for real.
This also blunts work that lands elsewhere: PR for #5996 replaces that ENOENT with a self-diagnosing message naming the unresolved import, and under the real plugin stack that message is masked too — zero occurrences in the log, before and after. The improvement is correct but invisible until this is handled.
Possible shapes (not a recommendation)
- Have the counter-probe stand down when the build has already errored — the condition its own message already describes ("this build never got far enough"). Detecting that from
closeBundleneeds a flag set by abuildEnd/renderErrorhook, since the probe cannot otherwise tell "zero because fixed" from "zero because dead". - Or keep it unconditional but make it non-masking: emit as a warning when a prior error is known, so the real error still reaches the top.
- Either way the pinned-drift signal the probe exists for must survive on a successful build, which is the case it was written for.
Activity
yinlianghui-tw commentedon Aug 24, 2026
PM claim + dispatch order — R15, with the shape ruled
Claimed by the
domain:devx@ objectui execution seat (#5748), round R15, PM sessionsession_019b5UBNMtTzKbVtZZGvFuxe. Branch:claude/issue-6093-counter-probe-masking.This is the highest-value card of the round. Not because the fix is large, but because of what it currently costs: every console build that dies before the pinned warnings are emitted reports the ledger's counter-probe instead of its own cause. The developer gets a 45-line list of field widgets unrelated to the failure, and the message reads as ledger drift — so the first instinct is to go edit
DEFEATED_LAZY_FIELD_WIDGETS, which is exactly the wrong move and would break the ledger for real. A wrong signal that actively misdirects is worse than no signal.Unblocked as of now: PR #6095 merged at 17:20, so
apps/console/vite.config.tsis free.Ruling: shape 1 — the probe stands down when the build has already errored
Take the first option. It is the condition the probe's own message already names: "or this build never got far enough to emit them, in which case the number to distrust is the zero, not the list." The probe already knows this case exists; it just cannot currently detect it. Give it the means.
As the card says, that needs a flag set from a
buildEnd/renderErrorhook, becausecloseBundlecannot otherwise distinguish "zero because fixed" from "zero because dead". Those two readings are the whole point of the probe, so the flag is not a workaround — it is the missing input.⛔ Shape 2 (downgrade to a warning) is rejected as the primary fix: a warning on a failed build still adds 45 lines of noise above the real error, and on a successful build a warning would silently weaken the drift signal the probe exists for. Take it only as a fallback if shape 1 proves impossible in this plugin's hook ordering — and if you do, say so and explain why.
⛔ Do not delete the probe, do not touch
DEFEATED_LAZY_FIELD_WIDGETS, and do not change what the ledger pins.The pinned-drift signal must still fire on a successful build where the warnings genuinely did not fire. That is the case the probe was written for, and a fix that suppresses it there would trade a masking bug for a blind gate — strictly worse, and invisible.
So the deliverable is three measured states, not one:
The third row is the control. Without it you have not shown the probe still works — only that you turned it off.
Verification
OBJECTSTACK_CLIENT_DISToverride whose own dependencies do not resolve. Under the unmodified plugin list,grep -c ENOENTon the log is 0 — that is the before. The after must show the real error present, andgrep -cis the way to say so.ENOENT. So your "after" should surface that message, not the old one — rebase on currentmainand quote what actually appears. Its author noted the improvement is invisible until this card lands; your PR is what makes it visible, and demonstrating that is the strongest evidence this fix works.configLoader: 'native'into a per-invocation.vite-tempfile. emit-eager-closure-report crashes with ENOENT when a fully-unresolvable dependency lands in the vendor-objectstack chunk #5996's dev used the differing timestamped filenames in the two stack traces to prove neither run read a cached compile; do the same or better, and state it.⛔ Out of scope: the eager-closure report (#5996, landed),
OBJECTSTACK_CLIENT_DISTvalidation (#6094, dispatched in parallel — ⛔ do not touch it, another dev is on it), and the wider question of whether unresolved bare imports should fail the console build generally.⛔ Never
git stash— shared stack, other agents are using it now.os-dev-reporton the first line, and write any angle-bracket fragment in words — GitHub's sanitizer strips both HTML comments and short angle-bracket fragments, even inside fenced code blocks.Refs: #5325 (the ledger) · #5996 / PR #6095 (where this was found) · #6094 (sibling, parallel).
Generated by Claude Code
yinlianghui-tw commentedon Aug 24, 2026
os-dev-report
{ "issue": 6093, "status": "done", "branch": "claude/issue-6093-counter-probe-masking", "pr": "https://github.com/objectstack-ai/objectui/pull/6113", "premise_still_valid": true, "summary": "Took shape 1 as ruled: the closeBundle counter-probe in scripts/vite-ineffective-dynamic-imports.ts now stands down when the build has already failed, so the build's own error reaches the top instead of being replaced by 45 lines of field widgets. The ledger, DEFEATED_LAZY_FIELD_WIDGETS and the probe's failure message are untouched. ONE DEVIATION FROM THE RULING'S MECHANISM, and it is the load-bearing finding: a buildEnd/renderError flag alone does NOT fix this card's own reproduction. Measured on vite 8.2.1 + rolldown 1.2.3 via a harness driving real vite.build() runs — buildEnd is handed the error ONLY for build-phase (transform) failures; renderError fires ONLY for renderChunk failures; for generateBundle and writeBundle failures neither hook is told anything, and the card's repro is a writeBundle error from emit-eager-closure-report. So a third input was added: the plugin marks its own writeBundle, declared order: 'post' so the marker is not armed by a plugin ordered after it (measured both ways). 'closeBundle reached, writeBundle never ran' is 'this build did not finish'. build.write === false is the one build that legitimately reaches closeBundle with no writeBundle, so the marker is read as failure evidence only when the build was going to write — that direction keeps the probe ARMED rather than silently disarming it. A stand-down prints exactly one line saying the ledger was not checked and why; zero output would be indistinguishable from a gate that passed.", "tests": "THREE MEASURED STATES, real apps/console vite build, base a100f77d3, fix 1ccafd03d. Repro per the card: out-of-tree OBJECTSTACK_CLIENT_DIST override whose dependency @objectstack/core/logger is confirmed MODULE_NOT_FOUND. STATE 1 (build fails before warnings emit): before, unmodified plugin — grep -c 'UNRESOLVED BARE IMPORT' = 0, grep -c 'did NOT fire' = 1, 67 log lines; after — grep -c 'UNRESOLVED BARE IMPORT' = 1, grep -c 'did NOT fire' = 0, 44 log lines. grep -c ENOENT is 0 both sides, correctly: PR #6095 replaced the bare ENOENT, and #6095's message is what now reaches the top ('error during build: / Build failed with 1 error: / [plugin emit-eager-closure-report] / RolldownError: ... eager-closure member `@objectstack/core/logger` has no file in ... UNRESOLVED BARE IMPORT ... (imported by: assets/framework-CXWLXU9l.js)'). STATE 2 (succeeds, warnings fired): clean vite build BUILD_EXIT=0, probe silent, summary line unchanged ('43 ineffective dynamic imports, all pinned (objectui#5325): 27 via FieldEditWidget.tsx ...'), eager-closure byte-identical to the pre-change baseline (3298316 bytes gzipped, 52/508 chunks) — this also proves the order:'post' writeBundle actually runs under the console's full plugin array. STATE 3, THE CONTROL (succeeds, warnings did NOT fire): same override with the unresolved import left tree-shakeable, build completes (eager-closure report written, 52/508 chunks, 3284846 bytes) yet none of the 43 fire — probe STILL errors, full 43-line 'did NOT fire' message, exactly as today. CONFIG FRESHNESS: configLoader 'native' compiles the config into a per-invocation apps/console/node_modules/.vite-temp/vite.config.ts.timestamp-*.mjs; four builds named four distinct temp files in their stack traces (1787592587612-53c9ef75413af, 1787592900399-0b15cbc6a0c3a, 1787593428472-e3df3c03aa7f9, 1787593608193-d78954b1d01f9), and .vite-temp/ was verified EMPTY before and after each build — vite deletes the compile after importing it, so there is no cache a stale compile could come from. ABLATION / REVERSE VERIFICATION: predicted direction was 'the three stand-down tests go red, the controls stay green'. Ablated buildDidNotFinish() to a constant false; mutation confirmed ON DISK by grep counts (original string 1 then 0, ABLATED marker 0 then 1) rather than by any editor exit code; restored by a trap on EXIT INT TERM and the tree verified byte-clean afterwards (git status empty, ABLATED absent, original line present). Result: 3 failed / 21 passed — exactly STATE 1, 1b, 1c red; STATE 3, 3b and the write:false test green. No dist rebuild is involved: the suite imports the plugin from source (../vite-ineffective-dynamic-imports.ts), no package exports indirection. GATES, all on final HEAD 1ccafd03d, run from the repo ROOT: 'pnpm exec vitest run scripts/__tests__ --maxWorkers=2' — 'Test Files 67 passed (67) / Tests 1857 passed (1857)'; 'pnpm run type-check:scripts' (tsc -p tsconfig.scripts.json) exit 0; eslint --no-inline-config on the two changed files — 0 errors 0 warnings (from --format json); 'pnpm run check:control-bytes' — 'check-control-bytes: OK (scanned 5057 tracked text file(s); skipped 85 binary)'; 'node scripts/check-changeset-presence.mjs' — 'No source of a released package changed in this range, so no changeset is owed' (diff is scripts/ only, so no changeset and no empty-frontmatter declaration is owed either; objectui has no skip-changeset label and none was created). DECLARED NARROWING: repo-wide 'pnpm lint' was narrowed to the two changed files. Total read from eslint's own --format json (2 files); eslint here is not type-aware (no projectService and no parserOptions.project in the flat config), so this diff cannot move the verdict on any untouched file; the full farm runs in CI regardless. Exit codes were captured before any pipe throughout, and every verdict quoted above is the gate's own printed line. All heavy runs went through the shared container lock (scripts/pm/os-verify-lock.sh), verdicts: five ACQUIRED runs, longest hold 75s, longest wait 55s.", "open_questions": [], "out_of_scope_findings": [] }One thing the PM should weigh, recorded here rather than as a new card — the ruling specified the flag as
buildEnd/renderError. That pair is measurably insufficient for this card's own reproduction (awriteBundlefailure tells neither hook anything), so the fix adds a third input: the plugin's ownwriteBundleatorder: 'post'. It is still shape 1 — a flag set from hooks that are told whatcloseBundleis not — but it is not the two hooks named. The hook-by-phase table is in the PR body and in the source header. Shape 2 was not needed and was not taken.Generated by Claude Code
Generated by Claude Code