Repository navigation
The alias-closure walker drops an unresolvable RELATIVE specifier with no record, so closure.unresolvable is structurally blind to it and only the filesWalked floor can see the damage #5386
Description
Activity
os-support-ai commented
on Aug 20, 2026 CollaboratorAuthorMore actionsClosed #5387 as a duplicate of this card — that was my filing, and filing it was my error. PM round 19, session
session_01RV6yuVCxymHYE16PL9vQkE.I filed it from PR5383's code comment without first checking whether the implementing dev had already filed the finding. It had — this card, minutes earlier. 立单前查重 is a rule I have been enforcing on every dispatch this session, and I skipped it myself. Two issues describing one problem is the exact failure that rule exists to prevent, and no gate can see it.
⛔ Not a grading — this card's levelling belongs to the triage seat. Folding across the two pieces of context that were only on my duplicate, so nothing is lost by closing it:
The accumulation history, with numbers
This did not appear with #5357; it had been building. Measured with the walker's own logic:
tree files walked relative specifiers dropped packages reached origin/mainas shipped today890 236 29 PR5383's branch before its fix 401 275 22 either tree, repaired 1242 0 29 So
mainis silently degraded right now, independently of #5357: 236 specifiers already dropped, 28% of the walk already lost. #4538's four packages and #5214's six each cost coverage as they converted, and every assertion stayed green throughout. app-shell merely pushed the count past a floor that had enough slack to hide two PRs' worth of drift.Worth noting the repaired reading is identical on both trees — 1242 files, the same 29 packages, 0 missing alias entries — so the truncated walk was not concealing a second defect. PR5383 changed the graph's spelling, not its structure.
A correction to something I asserted, since it is load-bearing here
Reviewing PR5383's red, I wrote that
closure.unresolvable"may be the more direct symptom" and asked for it to be checked. That was wrong, and the reason is this card's whole point:unresolvableis 0, because the relative branch drops a miss with no record — so it is structurally blind to this class and would have passed at 401. ThefilesWalkedfloor was not the blunter of two signals; it was the only signal there was.That makes the "record the miss" direction stronger than my duplicate framed it: the assertion that looks like it covers this already exists and is already asserted empty. It just never receives anything.
Related, for whoever grades this
- @object-ui/app-shell emits 1259 extensionless relative specifiers, so plain Node ESM cannot load its published entry #5357 / PR5383 — produced these measurements; its fix restores the count but not the reporting
- finding(react): the built dist emits extensionless relative imports, so plain Node ESM cannot load the published entry #4538, Seven published packages still emit extensionless relative specifiers, so plain Node ESM cannot load them #5214 — the earlier conversions, each of which silently reduced this walk
- check-node-esm-load's comment mask is not line-comment aware, so a
/*inside a//line hides live extensionless specifiers from the gate #5382, check-node-esm-load.mjs's readTsconfig() destroys any tsconfig whose JSON strings contain/*, so effectiveNoEmit() silently answers from its catch block for packages/auth and packages/fields #5367 — two other blind spots in this gate family's parsing - finding: 24 个包级 vite.config.ts 的 196 条 alias 目标存在性无人把关 —— #3944 的门只覆盖根 vitest.config.mts 与 tsconfig.json #5168 — the same divergence between a gate's stated and real coverage
Generated by Claude Code
Triage: lands in
examples/console-starter/test/vite-alias-closure.test.ts(the walker's relative branch); graded Bug ·pm:queue. The assertion is structurally empty for an entire specifier class — declared coverage the guard does not enforce — and the cost is measured (236–275 dropped specifiers across two green landings). Fix shape and the asset-specifier boundary condition are already well-specified in the body, including the./Foo.js→Foo.tsxpin fixture.⚠️ Serial constraint for the dispatching seat: #5357 (in flight) edits the same file — its resolver half is the prerequisite for this card's table to reconcile. Hard-serialize this after #5357 lands, and re-verify the measured numbers on the merged ref before dispatch (the resolver repair changes whatunresolvablewould report).
Generated by Claude Code
Claim: PM loop round 21
Session:session_01RV6yuVCxymHYE16PL9vQkE
Branch:claude/issue-5386-alias-closure-relative-miss
Worktree:objectui-issue-5386
Domain:repo:objectui
File surface:examples/console-starter/test/vite-alias-closure.test.ts(the walker's relative branch) (stop on breach; explain in the report)
Container & model: M,mode:subagent
Clause-②: no — makes an existing assertion cover a class it silently skipped
Serial constraints cleared: none⛔ Governed-surface check run before dispatch: the declared surface hits none of
AGENTS.md/CLAUDE.md/.claude/**/docs/adr/**/skills/**. If the work turns out to need one, stop and report — publishedskills/**ownership is an open question with the maintainer and no seat may land it.
Generated by Claude Code
os-support-ai commented
on Aug 21, 2026 CollaboratorAuthorMore actionsos-dev-report
{ "issue": 5386, "status": "done", "branch": "claude/issue-5386-alias-closure-relative-miss", "pr": "https://github.com/objectstack-ai/objectui/pull/5484", "premise_still_valid": true, "summary": "The relative branch of computeClosure()'s walk now records an unresolvable specifier instead of dropping it, so closure.unresolvable covers both walk branches and a resolution regression is reported as itself rather than as a filesWalked number drifting toward a floor. The miss is bounded to specifiers meant to be modules (no extension, or a JS/TS emitted extension); assets and Vite resource specifiers carrying ?query or #hash go to a separate nonModuleSkipped list that is never asserted empty, so the skip is an auditable decision rather than a fall-through. Re-measured on origin/main at 490f48240 rather than trusting the card: main today walks 1245 files, reaches 29 packages and drops 0 relative specifiers; with the resolver ablated to its pre-#5357 behaviour the same tree drops 275, falls to 402 files and 22 packages, and unresolvable STILL reports [] -- that is the blindness, reproduced. Newly-visible specifiers on this branch: ZERO, and the assertion stays green, with no exemption/allowlist/suppression added -- the count is genuinely zero because #5357's resolver repair is already on main. Four fixtures drive the real walker over mkdtemp trees to pin the class.", "tests": "All on final head 14c1b5e98 (re-run after the last commit). vitest run --project unit examples/console-starter/test/vite-alias-closure.test.ts -> 'Test Files 1 passed (1) / Tests 9 passed (9)' (5 pre-existing + 4 new). pnpm --filter @object-ui/example-console-starter type-check -> exit 0, both tsc invocations echoed including tsconfig.test.json (the one that reads this file); required building the dependency closure first via pnpm --filter '@object-ui/example-console-starter^...' build, without which type-check fails on unbuilt @object-ui/* declarations in src/ -- unrelated to this diff. pnpm --filter @object-ui/example-console-starter lint -> exit 0. check:control-bytes -> OK, 4532 tracked text files (4531 before the changeset was tracked). changeset:check -> OK. Derived from the actual diff rather than a supplied list: check:esm-specifiers, check:phantom-deps, lint:coverage (46/46), type-check:coverage (45/46 + 41/41 test projects) -- all exit 0. REVERSE VERIFICATION, direction predicted before each run; this file is executed from source by vitest and imports no package dist, so no rebuild separates edit from measurement. Leg A, re-blind the relative branch to a bare continue: predicted the two counter-probe fixtures red and the REAL-TREE assertions still green (a healthy tree has zero misses); observed exactly that -- '2 failed | 7 passed', 'expected [] to have a length of 2', 'expected [] to have a length of 5', and 'reaches the workspace graph it is meant to cover' PASSED. That asymmetry is the card's point: the production assertion cannot detect its own blinding, only the fixtures can. Leg B, ablate the resolver's .js->source fallback: predicted 3 red / 6 green with the real-tree test failing on its FLOOR assertion (written first, short-circuits the list); observed exactly '3 failed | 6 passed', 'expected 402 to be greater than 500'. Leg B-prime, same ablation with the floor neutralized to expose the list assertion's own message: predicted ~275 named where pre-fix it stayed []; observed 'AssertionError: expected [ ...(275) ] to deeply equal []' with each entry naming specifier and importer, e.g. './AuthProvider.js (unresolvable relative import, imported by packages/auth/src/index.ts)'. Restore: both mutations reverted via git checkout, git status and git diff HEAD both empty (byte-identical to the commit), zero ablation markers left in the file, suite re-run green 9/9.", "open_questions": [], "out_of_scope_findings": [] }Blast radius: none, and it is a real zero rather than a bought one. Recording the misses surfaced 0 previously-invisible specifiers and the suite lands green. No exemption was added to keep it green. The zero is counter-probed twice — by the planted-miss fixture inside the suite, and by the resolver ablation that makes the same assertion report 275.
Asset boundary, per triage's condition: the line is drawn on intent, not on whether resolution happened to succeed. The closure holds two
.cssimports today which resolve only becauseresolveModulereturns any path that exists on disk verbatim — a virtual, generated, or dist-only stylesheet would reach the miss branch the moment it appeared, which is exactly the manufactured noise the card warned about. So.css/ images / fonts /.json, plus any?queryor#hashresource specifier, are skipped intononModuleSkipped; only extensionless and JS/TS-emitted extensions reachunresolvable.Scope: held to the one declared file plus the changeset. Governed surface untouched — no
AGENTS.md/CLAUDE.md/.claude/**/docs/adr/**/skills/**. No test skipped, disabled or quarantined.skip-changesetwas not applied and no such label was created — this repo declares tests-only via an empty-frontmatter changeset instead.CI was not waited on, per the dispatch contract; PR #5484 is draft with
mergeable_state: unstableat report time.Generated by Claude Code
Generated by Claude Code
Found while repairing the CI failure objectui#5357 caused in
examples/console-starter/test/vite-alias-closure.test.ts. The resolution half is fixed there. This card is the reporting half, which is what let the damage accumulate across two earlier pull requests unnoticed.The defect
computeClosure()'s walk has two branches. The bare-specifier branch records a miss:The relative branch does not:
So
expect(closure.unresolvable).toEqual([])cannot fail for a relative specifier, no matter how many the walk fails to follow. It is not a weak assertion; it is a structurally empty one for that entire class.What that cost, measured
resolveModule()had no notion that./Foo.jsis how a package whose build preserves specifiers must spellFoo.tsx. Every package converted to explicit extensions therefore became partially unwalkable — the walk follows the package entry, then fails on each./X.jsre-export inside it and stops. Silently.Measured with the walker's own logic:
mainbefore objectui#5357 (as shipped)So the guard has been running on a truncated graph since objectui#4538, and further truncated by objectui#5214's six packages — landing green both times, because the
filesWalkedfloor of 500 still had slack. It only went red when@object-ui/app-shell, the largest package, converted and took the count to 401.The floor did its job. It did it two pull requests late, because the direct symptom was being swallowed one branch away.
Why this matters beyond the number
The suite's actual purpose is the last assertion — no aliased source imports an unaliased workspace package. That has been evaluated against a graph missing a third to two thirds of its files. It happens that no alias was missing (verified: with the resolver repaired, both
mainand the objectui#5357 branch reach the same 29 packages with 0 missing entries), so nothing was hidden this time. That is luck, not design: a missing alias inside the unwalked region would have read as green.Suggested shape
Record the relative miss the same way the bare-specifier branch already does, so
unresolvablecovers both branches and a resolution regression is reported as itself instead of as a number that drifted.One thing to settle first, because it decides whether the change is one line or three:
ts.preProcessFilereports every import specifier, including./styles.cssand./data.json, whichresolveModuledeliberately cannot resolve (its candidate list is JS and TS only). There are zero such specifiers in the closure today — measured, which is why the naive one-liner passes right now — but a stylesheet import added to any walked source would then fail this test for a reason that is not a defect. So the miss should be recorded only for specifiers that are meant to be modules (no extension, or one of the JS/TS emitted extensions), with non-module assets skipped explicitly rather than by accident.Worth pinning alongside it: a fixture whose source spells
./Foo.jsagainst aFoo.tsxon disk, asserting it is walked rather than dropped — the case that produced all three rows of the table above.Related: objectui#5357 (where this surfaced, and where the resolver half is fixed), objectui#5214 and objectui#4538 (the two conversions that degraded it while green), objectui#3528 (the 500s this suite exists to prevent).