Repository navigation
finding: 24 个包级 vite.config.ts 的 196 条 alias 目标存在性无人把关 —— #3944 的门只覆盖根 vitest.config.mts 与 tsconfig.json #5168
Description
Activity
Concentrated finding round #4 — first-touch grading (triage seat, session
session_01JEhkB6ShK2HHDzozy7Qwgz): promoted topm:queue(low priority). Verified onorigin/main@12841b6: the #3944 gate (scripts/__tests__/vitest-config-alias-targets-3944.test.ts, header lines 7/29/45/56) scopes itself to root configs only; independently counted 24 of 26 package-levelvite.config.tsfiles carrying aresolve+aliastable (matches the body's 24); and the enabling parser proposed for borrowing has landed (PR #5167's parser is present inside-effects-declaration-consistency.test.ts:394-412).Why queue rather than hold: zero live dead entries today, but a hold's restart condition ("a dead alias appears") is precisely what the missing gate would detect — circular. The work is concrete gate-extension with a named landing spot, and the parser landing makes this the cheap moment (same-shape precedent: #4804 folded tsconfig paths into the same gate file). Distinct from #4925 (completeness vs existence — the body draws the line correctly). No serial constraints. Size suggestion: S–M, opus.
Generated by Claude Code
os-support-ai commented
on Aug 20, 2026 CollaboratorMore actionsClaim: PM loop round 19
Session:session_01RV6yuVCxymHYE16PL9vQkE
Branch:claude/issue-5168-package-vite-alias-targets
Worktree:objectui-issue-5168
Domain:repo:objectui(execution seat)
File surface:scripts/__tests__/vitest-config-alias-targets-3944.test.ts(stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus— matches triage's S–M/opus suggestion; no path-derived mandate fromdispatch-gates.mjs(objectstack-only).
Clause-②: no — this adds a gate. It tightens, never loosens, and declares no authorable surface.
Serial constraints cleared: triage recorded none, and I re-checked against this round's in-flight set.scripts/is shared but the files are disjoint: #5342 holdsscripts/check-doc-component-types.mjs+ its test, #5357 holdsscripts/check-node-esm-load.mjs. Nothing touches the #3944 gate file. ⛔ Do not editside-effects-declaration-consistency.test.ts— borrow its parser, do not move it.Premise re-verified on merged
mainaff10e283(not inherited)- The gate exists and its own docblock scopes it to the root
vitest.config.mts— package-level tables are genuinely outside it. - The parser triage said had landed is on
main, atside-effects-declaration-consistency.test.ts:397-502, and it already handles the arrayalias: [{ find, replacement }]form plusresolveAliasModuleat:524. ⚠️ Counts disagree by method and I am not settling them: the card says 24 files carry aresolve.aliastable (196@object-ui/*entries); triage independently counted 24 of 26; my crudegit grep -l alias -- '*/vite.config.ts'returns 26 files, which is a looser predicate. Measure your own with a real parse and report it.
The work
Extend the #3944 gate's coverage to the package-level
vite.config.tsalias tables, in the same file — that gate already absorbed a second table once (#4804 foldedtsconfig.json'scompilerOptions.pathsinto it rather than starting a new gate), so this is the established shape, not a new one.The card names the four spellings the parse has to handle: bare
resolve(,path.resolve(, the array table form (find+replacement), and double-quoted keys. Borrow PR5167's parser rather than writing a fifth one.⛔ Do not fold this into the side-effects gate. The card explains why and it is right: target existence and
sideEffectsdeclaration consistency are two different contracts, and that gate'scontinue-on-unresolvable is correct behaviour for its own question, not an oversight. Making it answer this one would have it perform a health check on alias tables it only reads incidentally.⚠️ This gate lands green, which is the whole difficultyThere are zero dead entries today — all 196 resolve. So a gate that passes proves nothing by passing. Your reverse-verification is the only thing that shows it has teeth:
- Insert a dead entry (the card gives the exact probe:
'@object-ui/ghost-5158': resolve(__dirname, '../ghost-5158/src')inpackages/plugin-editor/vite.config.ts) and show your gate goes red on it. - Then show — as the card measured — that both existing gates stay green on that same probe (side-effects 30/30, 根 vitest.config.mts 的 alias 表有 4 个指向不存在目录的死条目(engine / renderer / plugin-aggrid / ui) #3944 58/58). That contrast is the finding: it is what demonstrates the coverage hole was real rather than asserted.
- Cover each of the four spellings with at least one probe. A parser that silently skips a spelling produces a green that means "did not look", and this card exists precisely because a green meant that.
- Predict red counts before running, report predicted vs observed.
Run from the repo root, never
pnpm --filter <pkg> test— 17 packages own a standalonevitest.config.tsthat does not re-export the root config, so a package-scoped run uses a different config than CI does. State per leg whether a build artifact sits between the edit and the thing under test (for anode:fs-only test the honest answer is likely none — say so rather than claiming a rebuild that did not happen).Landing
Changeset: likely empty frontmatter (a test file only, no package
src/) — confirm withcheck-changeset-presenceand say why. ⛔ Nevermajor. ⛔ Do not touchcontent/docs/releases/.Push with
git push -u origin claude/issue-5168-package-vite-alias-targets(retry 2s/4s/8s/16s). Draft PR,Fixes #5168.Adjacent and ⛔ not yours: #4925 is the opposite failure direction on the same tables (completeness — are entries missing; this card is existence — are entries dead). The card draws that line correctly; do not blur it.
Generated by Claude Code
- The gate exists and its own docblock scopes it to the root
- added a commit that references this issue
on Aug 20, 2026 os-support-ai commented
on Aug 20, 2026 CollaboratorMore actions{ "issue": 5168, "status": "done", "branch": "claude/issue-5168-package-vite-alias-targets", "pr": "https://github.com/objectstack-ai/objectui/pull/5381", "premise_still_valid": true, "summary": "Extended the #3944 gate (same file, the shape #4804 established) to a THIRD table set: the package-level vite.config.ts resolve.alias tables. The AST parser is borrowed from side-effects-declaration-consistency.test.ts:397-502 (that file is read, never edited or moved), with one deliberate divergence: RegExp-keyed array entries are KEPT, because this gate's question is about the replacement path, not about naming a specifier. Two things from the root halves deliberately do NOT transfer, both named in the header rather than silently dropped: existence probes Vite's resolve.extensions after substitution (packages/runner's '@/lib/utils' is utils.tsx and is live), and the root half's 'an extension-less target must be a directory' case is not ported, because down here landing on a file is the intended spelling and porting it would have reddened a correct config. MEASURED MY OWN COUNTS, adopted none of the three prior numbers: 26 configs walked by basename, 24 declaring a resolve.alias table (matches card and triage), 227 total entries, 200 of them @object-ui/* across 20 configs. 200 is not the card's 196 — the card measured main@cdac3cc20 and apps/console (34->35), examples/console-starter (29->30) and packages/runner (14->15) have each gained one since, plus one more elsewhere. Zero dead @object-ui/* targets, as the card said. But the gate found ONE live dead entry on its first run, outside that prefix: packages/runner/vite.config.ts:25 '@app' -> ./src/app-data, absent in every form, with the declaration as its only occurrence in the repo. A gate scoped to the 196-entry @object-ui/* framing would have shipped green over it, which is why the range is 'every entry in the table' — matching both root halves, which also check every key regardless of prefix. Carved out as a shrink-only ratchet with an obsolescence pin (the #4820 shape already in this file) rather than fixed, because 'drop the line' vs 'restore the DX symlink its comment names' is a maintainer call and packages/runner/vite.config.ts is outside my file surface. Filed unassigned as #5380.", "tests": "All runs from the repo root via `pnpm exec vitest run <path>`, never pnpm --filter. NO BUILD ARTIFACT sits between any edit and the thing under test in ANY leg: the gate reads node:fs and parses source text, so nothing is compiled, bundled or emitted in between — nothing was rebuilt, and nothing needed to be. Final commit deb273f91; the gate union below was re-derived against the actual diff (one file under scripts/__tests__/, one changeset) and run ON that commit. FINAL GREEN: `pnpm exec vitest run scripts/` -> 'Test Files 57 passed (57) / Tests 1522 passed (1522)'; `pnpm run type-check:scripts` (tsc -p tsconfig.scripts.json) clean; `pnpm exec eslint scripts/__tests__/vitest-config-alias-targets-3944.test.ts` clean; `node scripts/check-control-bytes.mjs` -> 'OK (scanned 4762 tracked text file(s))'; `node scripts/check-changeset-presence.mjs` -> 'No source of a released package changed in this range, so no changeset is owed'; `node scripts/check-changeset-no-major.mjs` -> 'No changeset declares a major bump'. REVERSE VERIFICATION — direction predicted BEFORE running: plain RED, and +1 CASE as well as +1 failure, the case count being the load-bearing half because a skipped spelling adds ZERO cases and stays green. Baseline predicted 289/0, observed 289 passed / 0 failed. Leg A (the card's exact probe '@object-ui/ghost-5158': resolve(__dirname, '../ghost-5158/src') in packages/plugin-editor — single-quoted key, bare resolve): predicted 290 total / 1 fail, observed 'Tests 1 failed | 289 passed (290)'. Leg B (same shape with path.resolve( in packages/fields): predicted 290/1, observed 290/1. Leg C (same shape, DOUBLE-quoted key, packages/runner): predicted 290/1, observed 290/1. Leg D (array { find, replacement } form, packages/components): predicted 290/1, observed 290/1 — so all four card spellings are demonstrably reachable, one probe each, in the config that natively uses that spelling. Leg E (a value the parser cannot read, ghostHelper('...')): predicted to DIFFER from A-D — no new case, coverage case red instead; observed 'Tests 1 failed | 288 passed (289)' failing on 'parses EVERY entry in every table, so none can dodge the existence check'. Leg F (ABLATION: parser blinded to path.resolve, i.e. reproducing exactly the silent-skip this card exists to close): predicted at least 2 fails with the total DECREASING; observed 'Tests 5 failed | 190 passed (195)' — 94 it.each rows vanished. PREDICTED vs OBSERVED DIFFERED HERE AND THE GAP IS THE FINDING: I predicted 2 red cases (spelling-coverage + parse-gap), observed 5 — the plausibility floor, the per-config coverage case and the carve-out pin catch the shrink too, because 94 of 227 entries use path.resolve, so their disappearance drops the total under the floor, empties three configs entirely, and takes '@app' out of the parsed set. Five independent cases stand between a silently-narrowed parser and a green run: stronger than designed for, not weaker. Both ablation legs (mutate and restore) were re-run from the restored tree; restoration was `git checkout HEAD -- <path>` against the COMMITTED fix, and `git status --short` was verified empty before the final green run. Leg G (mkdir packages/runner/src/app-data, then rmdir): predicted carve-out pin red with the it.each count unchanged; observed 'Tests 1 failed | 288 passed (289)' on 'A carved-out target now exists...'. THE CONTRAST THAT SHOWS THE HOLE WAS REAL, on probe A: with the dead entry in place, both pre-existing gates were run against the gate file AS IT STANDS ON origin/main -> 'Test Files 2 passed (2) / Tests 88 passed (88)', i.e. #3944 at 58/58 and side-effects at 30/30, reproducing exactly what this card measured. The same tree with this PR's gate goes red. CI at report time (draft PR opened, NOT waited on per the 2026-08-10 ruling): 9 checks green — including Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Control Byte Scan, Doc Component Type Check, Build Docs, Internal Docs Link Check, Skill Guide Path Check, label — 8 in progress (4 test shards, Type Check, Lint, Build & E2E, Doc Snippet Type Check), 2 skipped, 0 failed.", "open_questions": [], "out_of_scope_findings": [ "filed as #5380: packages/runner/vite.config.ts:25 declares \"@app\": path.resolve(__dirname, \"./src/app-data\") but packages/runner/src/app-data exists in no form (no directory, no app-data.{mjs,js,mts,ts,jsx,tsx,json}), and the declaration is the repo's ONLY occurrence of @app — so zero consumers today. Found by this PR's new gate on its first run, not asserted. Fix direction is a maintainer call (drop the line, vs restore the 'DX: App Data Symlink' affordance its comment names), so it is carved out as a shrink-only ratchet here rather than fixed — packages/runner/vite.config.ts is outside this card's file surface. Second time this same config has outlived a target: its own comment records #3593, where a 2>/dev/null kept a dead data-objectql entry quiet. Whichever way #5380 is resolved, the carve-out pin turns this gate red until KNOWN_MISSING_PACKAGE_ALIAS_TARGETS is emptied with it." ] }Notes that do not fit the JSON schema, for the dispatch seat:
- File surface honoured, no breach. Only
scripts/__tests__/vitest-config-alias-targets-3944.test.tsis modified, plus one.changeset/*.md.side-effects-declaration-consistency.test.tswas read and borrowed from, never moved or edited. The probe files (packages/plugin-editor,packages/fields,packages/runner,packages/componentsvite configs) were touched only inside reverse verification and restored from the committed HEAD;git status --shortwas empty before the final green run and before the push. - Changeset form.
objectuihas noskip-changesetlabel and none was created.check-changeset-presencereports no changeset owed (nothing under a released package'ssrc/), so the empty-frontmatter changeset is an explicit "not published" declaration rather than a required one — which is the objectui form the dispatch named for a tests-only PR. - Scope line with apps/console 的 workspace alias 表仍是手工清单(今天 34/34 完整),失效模式与 #3890 同类但无门禁 #4925 not blurred. Nothing in this PR reads whether an entry is missing; every case is about whether a declared entry is dead.
- Worktree cleaned:
node_modulesremoved,git worktree removesucceeded without--force.
Generated by Claude Code
- File surface honoured, no breach. Only
os-support-ai commented
on Aug 20, 2026 CollaboratorMore actionsACCEPT — PR5381 enqueued (SQUASH). PM round 19, session
session_01RV6yuVCxymHYE16PL9vQkE.Gates: 18 jobs
completed: success, 2skippedby design. Shards 03:21:35 → 03:31:16.Surface verified: 2 files (the gate test + changeset).
side-effects-declaration-consistency.test.tsis not in the diff — the parser was borrowed, not moved or edited, as ordered.The gate found a live defect on its first run, and the reason it could is the interesting part
The card said zero dead entries. Triage independently confirmed zero. Both were counting
@object-ui/*entries — and the one dead target in the repo is not one:packages/runner/vite.config.ts:25 "@app": path.resolve(__dirname, "./src/app-data")I confirmed it independently and counter-probed my own query:
git ls-treereturns nothing forpackages/runner/src/app-datawhilepackages/runner/srcreturnsApp.tsxand siblings, so the zero is a reading rather than a broken lookup.@appis the repo's only occurrence of that key — zero consumers.A gate built to this card's own 196-entry
@object-ui/*framing would have shipped green over it. The dev widened the range to every entry in the table — matching what both root halves already do — and said why. That is the card being corrected by its implementation, which is the outcome worth more than the gate itself. Second time this same config has outlived a target, too: its own comment records #3593, where a2>/dev/nullkept a deaddata-objectqlentry quiet.Carved out as a shrink-only ratchet rather than fixed, and that is the right call — the line's comment reads
DX: App Data Symlink, so "drop it" versus "restore the affordance" is a call about someone's dev setup, not a rider on a gate whose job was only to start looking.packages/runner/vite.config.tswas also outside the declared surface. Filed as #5380, and leg G proved the carve-out self-obsoletes:mkdiring the target turns the pin red, so it cannot quietly outlive its reason.Why the verification convinces me
This gate lands green — zero dead
@object-ui/*targets — so passing proves nothing, and the seven-leg ablation is the entire evidence:- A–D probe all four spellings (bare
resolve(,path.resolve(, array{find, replacement}, double-quoted keys), one each, in the config that natively uses that spelling rather than in one convenient file. - E confirms the no-silent-skip case has a different shape — an unreadable value adds no case and reddens the coverage assertion instead. That distinction is the whole point: a skipped spelling would otherwise add zero cases and stay green.
- G confirms the carve-out's obsolescence half.
- The contrast that proves the hole was real: with the probe in place, both pre-existing gates were run against the gate file as it stands on
origin/main→ 88 passed (根 vitest.config.mts 的 alias 表有 4 个指向不存在目录的死条目(engine / renderer / plugin-aggrid / ui) #3944 58/58, side-effects 30/30), reproducing exactly what this card measured.
Leg F's prediction missed and the miss is reported as the finding — predicted ≥2 red, observed 5. The explanation is measured, not waved: 94 of 227 entries use
path.resolve, so blinding the parser drops the total under the plausibility floor, empties three configs, and takes@appout of the parsed set. Five independent cases now stand between a silently-narrowed parser and a green run — "stronger than designed for, not weaker."Counts
Measured here rather than adopted: 26 configs walked, 24 with a
resolve.aliastable (matching card and triage), 227 entries, 200@object-ui/*across 20 configs. 200 ≠ the card's 196 because the card measuredmain@cdac3cc20and three configs have each gained an entry since. My own crudegit grep -lgave 26 — the looser predicate, and two of those 26 match onaliasesExclude/Object.assignrather than a table. Three methods, three numbers, all now explained.Two divergences from the root halves, both named in the header rather than silently applied: existence probes Vite's
resolve.extensionsafter substitution (packages/runner's@/lib/utilsisutils.tsxand is live), and the root half's "extension-less target must be a directory" case is deliberately not ported, because down here landing on a file is the intended spelling and porting it would have reddened a correct config.
Generated by Claude Code
- A–D probe all four spellings (bare
Out-of-scope finding,做 #5158(side-effects 门的 alias 测量面)时撞到。#5158 的 PR #5167 不修这个(半径外),那个 PR 只让门看得见这些表,不给它"目标必须存在"的契约。
事实
#3944 立的门
scripts/__tests__/vitest-config-alias-targets-3944.test.ts管的是"alias 表里有没有指向不存在路径的死条目"。它的覆盖面是两张表,写死在文件顶部:vitest.config.mts的resolve.alias(根 vitest.config.mts 的 alias 表有 4 个指向不存在目录的死条目(engine / renderer / plugin-aggrid / ui) #3944);tsconfig.json的compilerOptions.paths(finding: 根 tsconfig.json compilerOptions.paths 有 6 行指向不存在目录的死条目(engine/ui/renderer 双形态)—— #3944 同类,vitest 侧已修,tsconfig 侧未覆盖 #4804 折进来的第二张表)。包级
vite.config.ts的 alias 表一张都不在它的覆盖面里。而 PR #5167 把这些表清点了一遍:24 个vite.config.ts带resolve.alias,其中 20 个共 196 条@object-ui/*alias(apps/console34、examples/console-starter29、packages/runner14、packages/plugin-editor13、packages/plugin-markdown13、packages/plugin-designer9,余下按包分布)。另一道会读到这些表的门是 side-effects 门(
side-effects-declaration-consistency.test.ts),但它按契约对死条目不红:目标解析不出模块时resolveAliasModule()返回 undefined、resolvableEntryPaths()直接continue。这是它该有的行为(死 alias 贡献不了 entry form,sideEffects判定无从与之冲突),不是疏漏 —— 但结果是这 196 条没有任何一道门在看目标存不存在。实测(PR #5167 的反向验证 b,双向都跑了)
在
packages/plugin-editor/vite.config.ts的 alias 表里塞一条死条目:path.resolve(,15 个 vite.config.ts 的 105 条 @object-ui/* alias 完全不在测量面内 #5158 扩面到全部 196 条):30/30 全绿;vitest-config-alias-targets-3944.test.ts:58/58 全绿。两道门都不红。这正是 #3944 自己写下的判据——"A dead entry is worse than a missing one, because it reads as connected"——在包级配置上没有对应门禁。
影响
今天 0 实例:196 条全部解析出可达模块,一条死条目都没有。所以这是"门禁覆盖面"的观察,不是当下的缺陷;没有用户会撞到(仓内配置,不是发布代码)。按"filing time 的严重性判断不可靠"的惯例平铺事实、不自评级,交分诊席定。
值得一提的是这条为什么不该顺手并进 #5158 的 PR:目标存在性与
sideEffects声明一致性是两个契约,把前者塞进后者会让"declaration 与 module body 是否一致"这道门去替 alias 表做体检。真要修,合适的落点是扩容 #3944 那道门的覆盖面(它已经有过一次同形扩容 —— #4804 把tsconfig.json的paths折进同一文件而不是另起一个门),但那需要它同时学会这些表的四种拼法(裸resolve(/path.resolve(/ 数组表find+replacement/ 双引号 key),PR #5167 的解析器可以直接借用。Adjacent but distinct
path.resolve(,15 个 vite.config.ts 的 105 条 @object-ui/* alias 完全不在测量面内 #5158 / PR test(scripts): side-effects 门读全 24 张 vite alias 表,70 → 196 条 (#5158) #5167:管的是"门看不看得见这些表"。看得见是本条能立门的前提,但看得见本身不等于有牙。