Skip to content

fix(js): resolve Node subpath imports via package.json imports - #3382

Open
julien-e wants to merge 1 commit into
Graphify-Labs:v8from
julien-e:fix/package-json-subpath-imports
Open

fix(js): resolve Node subpath imports via package.json imports#3382
julien-e wants to merge 1 commit into
Graphify-Labs:v8from
julien-e:fix/package-json-subpath-imports

Conversation

@julien-e

@julien-e julien-e commented Sep 7, 2026

Copy link
Copy Markdown

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, #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 (#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 (#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

`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

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_importgraphify/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 emptygraphify/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 stargraphify/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 #importsgraphify/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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_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":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_resolve_js_import_target()

8 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant