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 }}
Repository navigation
TypeScript: unresolved calls bound to same-named exports in unimported packages → phantom cross-package calls edges (conf 0.8) #1659
graphify's call resolver attributes an unresolved call to a same-named export in an unrelated package that was never imported, producing a phantom cross-package calls edge at confidence 0.8. On a monorepo this fabricates dependency relationships that don't exist.
Observed: a cross-package edge pkg-a.run() --calls--> pkg-b.validate() (confidence 0.8). Expected: no cross-package edge — pkg-a has no import path to pkg-b, so the call cannot resolve there. It should stay unresolved (or ambient) rather than being bound to an unrelated package's export by name alone.
Real-world impact
On a ~14-package TypeScript monorepo (Apache-2.0, open-design), this produced edges like platform → registry-protocol and sidecar → registry-protocol even though platform and sidecar import nothing (verified against package.json and every import statement). registry-protocol exports many generically-named symbols (*Schema, etc.), so ambiguous/unresolved calls across the repo collapsed onto it — it appeared "depended on" by 14 packages spuriously. A reader trusting the graph would conclude a dependency direction that does not exist; only cross-checking against source caught it.
Likely root cause
When a call has no local definition and no matching import, the resolver appears to fall back to any same-named exported symbol repo-wide, and emits the edge at 0.8 rather than leaving it unresolved. The fallback isn't gated on an actual import/module-resolution path existing between the two files.
Suggestion
Gate cross-file/cross-package calls attribution on there being a real import (or module-resolution) path from caller to callee; when the only candidate lives in a package with no such path, leave the call unresolved (or emit it as ambiguous/inferred at markedly lower confidence) instead of a 0.8 calls edge. A diagnose-style warning for "cross-package edge with no supporting import" would also help users spot these.
Fixed in 62b8eb1 (on v8, ships in 0.9.7). Thanks for the precise root-cause and the real-world open-design example — that made the fix straightforward.
Your diagnosis was exactly right: when a callee had a single same-named definition repo-wide, the cross-file resolver bound to it at INFERRED/0.8 with no import path required. The fix takes your first suggestion, scoped by language: JS/TS modules have no implicit cross-module scope, so a direct cross-file calls edge is now gated on import evidence (a real import/module-resolution path from caller to callee) and left unresolved otherwise. Your pkg-a.run() -> pkg-b.validate() repro now produces no edge; a genuinely-imported call still resolves (and is promoted to EXTRACTED).
Scope notes:
Other languages keep the existing single-candidate resolution — C/C++ headers, Ruby autoload, and same-package implicit scope legitimately call across files without an explicit import, so gating them would drop real edges.
The indirect_call path (callbacks passed by name) is unchanged; it was already INFERRED and gated on the target being a real callable.
While in there I also hardened the caller/candidate → file mapping so a path-resolution/symlink mismatch can't spuriously fail import evidence — a side benefit is that more legitimately-imported cross-file calls now land as EXTRACTED rather than INFERRED.
A diagnose-style 'cross-package edge with no supporting import' warning is a nice idea for a follow-up; leaving it out of this fix since the edges are no longer emitted in the first place.
Activity
fix(extract): gate JS/TS cross-file calls on import evidence to kill …
safishamsi commentedon Jul 4, 2026
Fixed in
62b8eb1(on v8, ships in 0.9.7). Thanks for the precise root-cause and the real-world open-design example — that made the fix straightforward.Your diagnosis was exactly right: when a callee had a single same-named definition repo-wide, the cross-file resolver bound to it at INFERRED/0.8 with no import path required. The fix takes your first suggestion, scoped by language: JS/TS modules have no implicit cross-module scope, so a direct cross-file
callsedge is now gated on import evidence (a realimport/module-resolution path from caller to callee) and left unresolved otherwise. Yourpkg-a.run() -> pkg-b.validate()repro now produces no edge; a genuinely-imported call still resolves (and is promoted to EXTRACTED).Scope notes:
indirect_callpath (callbacks passed by name) is unchanged; it was already INFERRED and gated on the target being a real callable.A
diagnose-style 'cross-package edge with no supporting import' warning is a nice idea for a follow-up; leaving it out of this fix since the edges are no longer emitted in the first place.docs: complete the upstream audit with full file-by-file diff + verif…
fix: symlink-containment guard + two JS/TS cross-file false-edge bugs
release: 0.9.7
bump: version to 0.17.0
fix(extract): gate JS/TS cross-file calls on import evidence to kill …
release: 0.9.7
fix(rll): reach calls written at module scope