Skip to content

fix(call-graph): TS/JS spread qualifier, namespace-import aliases, package self-reference (#3787 #3788 #3789) - #4261

Merged
squid-protocol merged 11 commits into
mainfrom
fix/ts-resolver-3787-3788-3789
Oct 3, 2026
Merged

squid-protocol merged 11 commits into
mainfrom
fix/ts-resolver-3787-3788-3789

Conversation

@squid-protocol

@squid-protocol squid-protocol commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Three TS/JS call-graph and import-graph fixes, measured against the TypeScript compiler reference (zod, the only TS reference repo in tests/import_graph_corpus.json).

#3787: a spread ...f() is not a receiver

  • Root cause: _call_qualifier read the first dot of ... as a member-access separator with no identifier before it, so ...parseCaseValue(x) got qualifier <expr> and resolved as an untyped receiver (methods only), missing the same-file function.
  • Fix: a spread ends the receiver walk: ...f() is a bare call (''), ...ns.f() is a call on ns.
  • Test: test_calls_out_qualifiers_3329.py::test_spread_operator_is_not_a_receiver (fails without the fix).

#3788: ns.f() when the alias differs from the file name

  • Root cause: the import step matched a qualifier only against imported file stems/dirs; no alias to module map existed.
  • Fix: JS/TS files record namespace aliases (import * as, import x = require, const x = require) as file_data.namespace_imports (new nullable column with schema heal, rehydrated for delta scans; an alias bound to two different modules is dropped). The import pass resolves each to a file (NetworkRiskSensor.namespace_aliases), and ns.f() resolves at the import step to that module or what a barrel (index.*, 2 hops) re-exports. explain_call.py passes the aliases too.
  • Test: tests/core_engine/test_namespace_imports_3788.py (13 tests: capture, resolution, barrel, negatives, persist/rehydrate).

#3789: a package importing itself by name

  • Root cause: _resolve_path_mirror treated zod/v4 as an external package.
  • Fix: new core/package_self_reference.py. The nearest package.json name must equal the specifier's package, then exports (subpaths, * patterns, conditions in declaration order, null hides; with no exports: main/module/the subpath) names the target, which must be a scanned file (.js maps to .ts). Another package, an undeclared subpath, or a nameless/nested manifest resolves to nothing. Reads via source_text.read_source, bounded (1 MB, depth 6, 24 targets, per-directory cache). Needs the scan root (set by galaxyscope). Workspace siblings are not handled.
  • Test: tests/core_engine/test_package_self_reference_3789.py (18 tests).

Measurements (zod, vs tsc 6.0.2)

confident precision strict precision confident judged recall resolution recall
main 99.9 96.6 1520 57.1 73.7
+ #3787 99.9 96.7 58.6 75.8
+ #3788 99.9 96.8 60.6 78.4
+ #3789 99.9 96.8 1614 60.6 78.4

Confident precision does not drop; ambiguous precision stays 32.1. #3789 adds nothing to the call graph by itself: the 133 z.x() calls need the self-import edge AND a re-export chain (see follow-ups). Its evidence is the import graph:

import graph (zod) resolvable imports engine edges precision recall
main 666 629 100.0 100.0
branch, old reference 666 817 77.0 100.0
branch, reference scores self-reference 859 817 100.0 100.0

The 188 new engine edges all read as false positives because the tree-sitter reference treated every bare specifier as external. Samples are real (import { z } from "zod/v3" in src/v3/benchmarks/*.ts to src/v3/index.ts, declared by package.json exports). The reference now implements Node self-reference independently (_self_reference in import_graph_accuracy.py). javascript (express) was not measured: the corpus repo is not fetched here.

Baselines

  • call_graph_resolution_typescript_baseline.json: recall 57.1 to 60.6, resolution recall 73.7 to 78.4, judged 1520 to 1614, strict precision 96.6 to 96.8.
  • import_graph_accuracy_baseline.json, typescript only: edges 629 to 817, imports 666 to 859 (hand-edited: --regenerate needs every language fetched).

Golden drift (both legs re-blessed with crucible_check --update, scope audited with bless_scope.py)

Gates

pr_gates.py: 7/7 pass (lint, audits, secrets, xray, gauntlet, golden, full suite) on the tree merged with main.

Follow-ups (not filed)

Closes #3787
Closes #3788
Closes #3789

🤖 Generated with Claude Code

https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8

squid-protocol and others added 10 commits October 3, 2026 13:52
_call_qualifier read the first dot of `...` as a member-access separator with
an empty identifier before it, so `...parseCaseValue(x)` got qualifier
`<expr>` and resolved as an untyped receiver (methods only), missing the
same-file function. A spread now ends the receiver walk: `...f()` is a bare
call and `...ns.f()` is a call on `ns`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
…3788)

`import * as processors from "./json-schema-processors.js"` binds an alias
that is not the file's name, and the resolver's import step matched a
qualifier only against imported file stems and directories. A JS/TS file now
records its namespace aliases (import * as, import = require, const = require)
as file_data.namespace_imports (persisted and rehydrated for delta scans); the
import pass resolves each to a file, and `ns.f()` reaches that module, or what
a barrel module re-exports, through the import step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
…ckage.json (#3789)

`from "zod/v4"` inside zod was a bare npm specifier, so a package's own tests
and examples drew no edge to its source and every `z.x()` call through that
import went unresolved. The nearest package.json's `name` and `exports` map
(or `main`/`module`, or the subpath itself when there is no `exports`) now
resolve a self-reference to the scanned file the manifest declares. A
specifier naming another package, an undeclared subpath, or a target that is
not a scanned file still resolves to nothing. Workspace siblings are not
handled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
Recall 57.1 -> 60.6, resolution recall 73.7 -> 78.4, confident judged
1520 -> 1614, strict precision 96.6 -> 96.8; confident precision stays 99.9.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
…3789)

The tree-sitter reference treated every bare specifier as external, so the 188
zod edges the engine now draws for `zod/v3` / `zod/v4` self-imports read as
false positives (precision 77.0%). It now resolves a specifier naming the
file's own package through the nearest package.json exports, independently of
the engine. zod: 859 resolvable imports, 817 engine edges (was 666 / 629),
precision and recall 100.0%.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
fp-ts TaskEither.getApplicativeTaskValidation now links to
Either.getApplicativeValidation (fan_out 0 -> 1, fan_in 3 -> 4, pagerank
ripple) via the spread/namespace fixes. Zero-dep leg also moves two tiny
pagerank ripples (racket fun.c, apollo WAITLIST.agc). Full leg: this
environment's own full-precision bless drifts from the committed one on
origin/main itself, so only the delta between a bless of main and a bless of
this branch was applied.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
…anifest keys (#3789)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
… fun.c, apollo WAITLIST.agc)

Matches the zero-dependency leg; blessed with crucible_check --update in the
gate's own venvs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
Golden masters taken from main; re-blessed in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
…ank ripples)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
Comment thread gitgalaxy/core/package_self_reference.py Fixed
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

🐦‍⬛ Muninn Security Scan

✅ No security issues found.

🐦‍⬛ Powered by Muninn · Skald Lab

…L empty-except)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sNMtAg6s9dyyjNgnjRHi8
@squid-protocol
squid-protocol merged commit a16001f into main Oct 3, 2026
36 checks passed
@squid-protocol
squid-protocol deleted the fix/ts-resolver-3787-3788-3789 branch October 3, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants