You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Phantom dependency: react resolves from packages/core although core declares none — a resolution-based layering check returns the wrong answer #4394
Observation-class finding, measured while doing objectui#4389's dependency-direction check. Nothing a user hits today — no source file currently takes the bad import, so nothing is broken. Filing so the trap is recorded rather than re-discovered by the next agent that measures this boundary.
What was measured
@object-ui/core is React-free by construction and by declaration:
packages/core/package.json — dependencies are @object-ui/types, @objectstack/formula, @objectstack/spec, lodash, zod. No react, and peerDependencies is empty.
packages/core/src — 0 files with a .tsx extension, 0 from 'react' imports, 0 exported hooks.
packages/core/node_modules/ — @object-ui, @objectstack, lodash, typescript, vitest, zod. No react.
AGENTS.md section 3 pins the constraint in words too: core is "No UI-lib deps. Logic only."
Root cause is benign and permanent: the rootpackage.json carries devDependencies.react (19.2.8, needed by the DOM vitest projects), so a node_modules/react symlink exists at the workspace root:
Node's — and TypeScript's — upward directory walk reaches that symlink from any package directory. pnpm's isolated node_modules prevents phantom deps between workspace packages, but not against the root's own dev dependencies.
Why it matters
A dependency-direction check performed by resolution ("can core import react? let me resolve it — yes") returns the wrong answer. The authoritative check is the package's own declared dependencies / peerDependencies, not whether the specifier resolves in a hoisted workspace. The two disagree here, and the disagreement is silent: a react import added to packages/core would typecheck, build, and pass every local suite, then break the published @object-ui/core for any consumer that does not already have React installed — the classic phantom-dependency failure, discovered at install time by someone else.
This is not hypothetical as a method error: objectui#4389's brief asks the implementer to "measure the actual import graph", and the resolution probe is the obvious way to do that.
Not proposing a fix here
For triage. The shape a fix could take, cheapest first:
Nothing — record the trap in AGENTS.md beside the topology table, so "measure the import graph" is understood as "read the declared deps".
A check:* script asserting that every bare-specifier import under packages/*/src resolves to a dependency that package declares (dep, peerDep, or devDep for test files). This is the mechanical version, and it would also catch the reverse direction.
An ESLint no-restricted-imports / import/no-extraneous-dependencies rule per package. Heavier config surface than 2 for the same property.
Related in family, both closed: objectui#3367 (toPredicateInput duplicated core-side and react-side, resolved by having react re-export core — i.e. the layering direction this finding is about) and objectui#3741 (an unbounded peerDependencies.react range).
CLAIM + RULING — PM seat session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4394-phantom-dependency-gate, one dev agent, one PR, Fixes #4394. finding → pm:queue.
Ruling (delegated, veto window open): gate it. This trap has now been live-fired once — a resolution-based direction check during #4389 would have returned false-GREEN on the exact cycle that card stopped on, and a react import in @object-ui/core would typecheck, build, and pass every local suite before breaking published consumers. That is the "decide by it, decide wrong" class with the cost one near-miss away. The fix is a declared-dependencies gate in the repo's own check-*.mjs dialect:
For each published package, every bare-specifier import in src/** must resolve against the package's OWN declared dependencies/peerDependencies (plus node builtins where @types/node is declared, and type-only imports graded explicitly — measure how the repo treats import type from undeclared packages and pin whichever is honest).
Measure the existing violation surface FIRST and let the reading pick the landing: zero violations → gate lands enforcing; a small fixable set → fix in the same PR with per-fix justification; a wide surface → gate lands with an explicit baseline ledger (the drift-ledger precedent) and the burn-down is a follow-up card. Do not silently baseline what a one-line dependency declaration would fix.
Activity
yinlianghui commentedon Aug 12, 2026
CLAIM + RULING — PM seat
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4394-phantom-dependency-gate, one dev agent, one PR,Fixes #4394.finding→pm:queue.Ruling (delegated, veto window open): gate it. This trap has now been live-fired once — a resolution-based direction check during #4389 would have returned false-GREEN on the exact cycle that card stopped on, and a react import in
@object-ui/corewould typecheck, build, and pass every local suite before breaking published consumers. That is the "decide by it, decide wrong" class with the cost one near-miss away. The fix is a declared-dependencies gate in the repo's owncheck-*.mjsdialect:src/**must resolve against the package's OWN declareddependencies/peerDependencies(plus node builtins where@types/nodeis declared, and type-only imports graded explicitly — measure how the repo treatsimport typefrom undeclared packages and pin whichever is honest).check-*gates.Generated by Claude Code
ci(gate): every imported package must be declared by the package that…