fix(js): resolve Node subpath imports via package.json imports - #3382
fix(js): resolve Node subpath imports via package.json imports#3382julien-e wants to merge 1 commit into
imports#3382Conversation
`import X from '#services/x'` — Node's subpath-imports field, and the way every AdonisJS 6 app reaches its own modules — was never resolved: the JS resolver only knew tsconfig `paths` and workspace packages. In a real Adonis app 438/680 alias imports produced no edge and `affected "ZitadelService"` listed 2 nodes from a stray root script instead of the 7 controllers and the auth middleware that depend on it. Resolve `#…` specifiers through the nearest package.json `imports` map, after tsconfig `paths` (explicit aliases keep precedence, Graphify-Labs#1269) and before workspace packages. Node semantics: exact key or a single `*` wildcard, longest literal prefix wins, condition objects go through _resolve_export_target, and only `./` targets are followed so an external mapping keeps the ref-namespaced external id (Graphify-Labs#1638). The map applies to every file in the package, so a nested tsconfig without the aliases (e.g. `inertia/tsconfig.json`) no longer shadows it. Cached per start dir and cleared per extract() run like the tsconfig caches (Graphify-Labs#2917). Measured on a 966-file AdonisJS + Inertia repo: alias imports resolved go from 242/680 to 680/680 in app/ and 17/17 in inertia/. Claude-Session: https://claude.ai/code/session_011z6vqqu1uujGKjjUE9Dgbg
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.
Formal verification. 3 change(s) tested, no difference found (not proven).
Graphify review — findings
Resolves Node subpath imports (package.json imports, e.g. #services/*) to local JS/TS files, so AdonisJS-style apps that reach their own modules through # aliases now produce cross-file edges instead of dead-ending. _resolve_package_import walks up to the nearest package.json with an imports map, applies Node's exact-or-single-wildcard ranking (stricter than tsconfig alias matching, so #config won't match #config/other), and follows only ./ targets — external values like "#dep": "dep-node-native" still fall through to external-reference handling. It runs after tsconfig paths so explicit aliases keep precedence, isn't shadowed by nested tsconfigs, and its cache (_PACKAGE_IMPORTS_CACHE) is cleared per run alongside the tsconfig caches.
Worth a look
- start_dir may be None but is passed to _resolve_package_import —
graphify/extractors/resolution.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Wildcard replacement drops target when captured segment is empty —
graphify/extractors/resolution.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Package import wildcard target replaces only the first star —
graphify/extractors/resolution.py:591· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _resolve_js_module_path assumes start_dir is non-None when handling #imports —
graphify/extractors/resolution.py:623· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1975 functions depend on the 369 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 callers, 43 callees - new:
_rebuild_code()— 113 callers, 50 callees - new:
_extract_generic()— 18 callers, 25 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 123 callees - new:
extract_objc()— 27 callers, 9 callees - new:
_resolve_js_module_path()— 33 callers, 7 callees - …and 41 more — each is listed as a finding
Verification — 1975 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1810 function(s) in the blast radius were not formally verified this run
Formal verification
No difference found (not proven): No behavior difference found in god\_nodes (not a proof).
The verifier ran both versions of god\_nodes on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_absolutize\_source\_files\_in.
The verifier did not have enough to check \_absolutize\_source\_files\_in, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_relativize\_source\_files\_in.
The verifier did not have enough to check \_relativize\_source\_files\_in, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in to\_canvas (not a proof).
The verifier ran both versions of to\_canvas on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify to\_obsidian.
The verifier did not have enough to check to\_obsidian, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_js.
The verifier did not have enough to check extract\_js, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_extract\_python\_rationale.
The verifier did not have enough to check \_extract\_python\_rationale, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_vue.
The verifier did not have enough to check extract\_vue, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_normalize\_ts\_import\_types (not a proof).
The verifier ran both versions of \_normalize\_ts\_import\_types on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_rescue\_js\_dynamic\_imports.
The verifier did not have enough to check \_rescue\_js\_dynamic\_imports, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_resolve\_cpp\_member\_calls.
The verifier did not have enough to check \_resolve\_cpp\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_resolve\_csharp\_member\_calls.
The verifier did not have enough to check \_resolve\_csharp\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_resolve\_java\_member\_calls.
The verifier did not have enough to check \_resolve\_java\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_resolve\_swift\_member\_calls.
The verifier did not have enough to check \_resolve\_swift\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_json.
The verifier did not have enough to check extract\_json, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_apply\_symbol\_resolution\_facts.
The verifier did not have enough to check \_apply\_symbol\_resolution\_facts, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_js\_exported\_declaration\_names.
The verifier did not have enough to check \_js\_exported\_declaration\_names, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_resolve\_cross\_file\_imports.
The verifier did not have enough to check \_resolve\_cross\_file\_imports, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: the input domain has 81 values but only 9 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
Could not verify: Could not verify \_resolve\_js\_module\_path.
The verifier did not have enough to check \_resolve\_js\_module\_path, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `start_dir` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify global\_add.
The verifier did not have enough to check global\_add, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `source_path` is annotated `Path` — outside the synthesizable primitive/collection set
· 2 grounded finding(s) anchored inline below; 47 more finding(s) on lines outside this diff (see the check run).
| resolved = _resolve_js_import_path(candidate) | ||
| return resolved if resolved.is_file() else None | ||
|
|
||
| def _resolve_js_module_path(raw: str | Path, start_dir: Path | None = None) -> Path | None: |
There was a problem hiding this comment.
_resolve_js_module_path()
fans out to 7 callees (efferent coupling); 33 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
| return _resolve_workspace_import(raw, start_dir) | ||
|
|
||
| def _resolve_js_import_target(raw: str, str_path: str) -> "tuple[str, Path | None] | None": |
There was a problem hiding this comment.
_resolve_js_import_target()
8 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
import X from '#services/x'— Node's subpath-imports field, and the wayevery AdonisJS 6 app reaches its own modules — was never resolved: the JS
resolver only knew tsconfig
pathsand workspace packages. In a realAdonis app 438/680 alias imports produced no edge and
affected "ZitadelService"listed 2 nodes from a stray root script instead of the7 controllers and the auth middleware that depend on it.
Resolve
#…specifiers through the nearest package.jsonimportsmap,after tsconfig
paths(explicit aliases keep precedence, #1269) andbefore workspace packages. Node semantics: exact key or a single
*wildcard, longest literal prefix wins, condition objects go through
_resolve_export_target, and only
./targets are followed so an externalmapping keeps the ref-namespaced external id (#1638). The map applies to
every file in the package, so a nested tsconfig without the aliases
(e.g.
inertia/tsconfig.json) no longer shadows it. Cached per startdir and cleared per extract() run like the tsconfig caches (#2917).
Measured on a 966-file AdonisJS + Inertia repo: alias imports resolved
go from 242/680 to 680/680 in app/ and 17/17 in inertia/.
https://claude.ai/code/session_011z6vqqu1uujGKjjUE9Dgbg