Skip to content

feat(graph): read C/C++, PHP, C#, Dart, components, Lua, GDScript imports, Swift at module level, and measure the edges (refs #980) - #1030

Merged
manojmallick merged 10 commits into
developfrom
feat/import-resolvers-980
Oct 26, 2026
Merged

manojmallick merged 10 commits into
developfrom
feat/import-resolvers-980

Conversation

@manojmallick

@manojmallick manojmallick commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

What this does

The import graph (--impact, get_impact, the blast radius in plan, doctor) now reads C/C++/Objective-C, PHP, C#, Dart, Vue/Svelte/Astro, Lua and GDScript (fifteen families, up from eight), and gives a module-level answer for Swift. It then measures how far those edges agree with what each language's own tooling resolves, and publishes the result, including the prediction that was missed.

Refs #980. This does not close it: C#, Dart, Lua and GDScript have fixtures and no accuracy figure (no reference could be run), and the git co-change reference is not done. Closing it is your call.

Shipped (src/, in the npm package: +29 KB packed, +2 files)

What it measured

Nine pinned repositories, each exported at its pinned commit, against clang -E -H (host, Linux, Windows), TypeScript's own module resolution behind the three framework parsers, Composer's own autoloader, and swiftc + SwiftPM. Over the importing files both sides read:

precision recall graph / reference edges
C / C++ (libuv, abseil-cpp) 99.7% 99.7% 4,609 / 4,611
PHP (laravel, composer) 100.0% 100.0% 4,976 / 4,975
Swift, module level (alamofire, vapor) 100.0% 99.3% 150 / 151
Vue / Svelte / Astro (vue-core, svelte, astro) 100.0% 79.1% 1,535 / 1,941

The protocol and eight predictions were committed (975d281) before the scorer ran on any evaluation repository. Seven held. P4 (component recall ≥ 90%) was missed, at 79.1%, and the page says so.

Why: every remaining component miss is in the resolver that all .js/.ts files share, not in the component reader. 390 of the 406 are in svelte, 389 of them files inside the svelte package importing "svelte", which TypeScript resolves through the package's own exports. The rest: ./x.js written for x.ts (12, astro), JSON modules (3), a #name import (1), and one pair of files whose names differ only in case (the evaluation ran on a case-insensitive filesystem). I did not change that resolver: the ranker's graph is built from it, so it needs its own gate review.

Also: C/C++ and PHP precision are lower bounds (an include behind a switch the reference did not run; a literal require the reference does not read). Laravel's Str.php has 167 direct importers, every PHP source file that names Illuminate\Support\Str (the four other grep hits are Blade templates).

Things you should know

  • A finding outside this issue, not fixed here: the JS/TS import matcher is line-based, so import {\n a,\n b\n} from './x' gives no edge in a plain .ts/.js file. Prettier-wrapped imports are common, so --impact on JS/TS files under-reports importers. A development run on vitepress found it; I read such lists in component scripts only (wrapped flag, pinned by a test and two mutants) and left the shared matcher as it was, because the ranker's graph is built from it and widening it moves retrieval. It deserves its own issue with its own gate review; I have not filed one.
  • Three evaluation runs, logged. The first run's figures are in the protocol's deviation log. The second added a "why was this edge missed" breakdown and reproduced the first exactly (checked field by field). Reading the misses then showed two defects in the new component code (an Astro file with a blank line before its --- fence lost its frontmatter; a recursive component was its own importer); I fixed both (91aec34) and ran a third time, which is the saved report. Against the first run: C, PHP, Swift are bit-for-bit unchanged; components went 99.7% → 100.0% precision (1,539 → 1,535 edges), 79.0% → 79.1% recall. P4 is missed either way.
  • The retrieval gate has no headroom, and this PR is at the floor. validate:retrieval hard is 63/90 = 70.0% exactly, with 0 tasks of headroom; the hard corpus scores this repository against itself, so adding indexed source moves it. The first layout of this code (nine files) flipped h089; a single file flipped h081 (via a benchmark-only moduleEdges that matched the query "edges"); the final layout flips none: hard 63/90, mined 68.3%, easy 90.0%, against develop's 63/90, 66.7%, 90.0% (one mined task moves from rank 6 to 5). 27 of 166 task ranks shift by a place or two. That is footprint, not design, and I chose the layout (one module; benchmark-only code moved out of the package) partly with the gate in mind. A gate that a refactor can flip deserves your attention.
  • xrepo no-regress holds: 116/208 (55.8%), won 0, lost 0, identical to the baseline, as the ranker is unchanged. The issue asked for movement on laravel/serilog/riverpod to be reported unedited, so I also measured the ranker handed the extended graph (report only, nothing shipped): 114/208 (54.8%), won 0, lost 2 (laravel x049, newtonsoft-json x109; p = 0.5, not distinguishable from reshuffling). serilog and riverpod do not move. That is the evidence for keeping the ranker on the original eight.
  • CI on Linux caught two bugs macOS hid, both fixed. (1) A product bug: the Dart resolver looked for pubspec.yaml in directories spelled as the graph's lowercased keys spell them, so on a case-sensitive filesystem every package: import of a package in the same repo resolved to nothing whenever a path had a capital letter (f502dd2). A new test runs the four resolvers that read project files (Dart, PHP, C#, GDScript) with fs behaving case-sensitively, so macOS now catches it too. (2) A benchmark bug: the Swift reference wrote swiftc's output to /dev/stdout, which Linux refuses when stdout is a socket (6725460); the Swift group re-run on macOS reproduces the saved report exactly. Both are in the protocol's deviation log.
  • Development runs changed the references, not a resolver rule tuned to a figure: the Swift reference reads main.swift as the entry point; the component reference keeps .d.ts targets; the C reference lost a stub-and-retry step that a mutation test showed never changed a result. All listed in the protocol.

Not in this PR

  • No figure for C#, Dart, Lua, GDScript (stated, with reasons, in the protocol and on the page).
  • No git co-change reference (the pinned clones are single-commit; needs its own method).
  • No change to the shared JS/TS resolver (exports self-reference, ./x.js → x.ts, JSON, #imports, .d.ts, import lists across lines). Each is a real gap for plain JS/TS too and would move the ranker's graph.
  • Not released. W4 (steps 10–12) goes out as a batch, and you decide when.

Test plan

  • test/integration/import-resolvers.test.js: 29 pass; 50/50 mutants of the resolvers and the builder wiring are killed
  • test/integration/import-accuracy.test.js: 22 pass with the reference tools (20 pass, 1 skip without them); 32/32 mutants of the arithmetic, the page renderer, the adapters and the --save guards are killed; the page, the saved report and every figure quoted in prose and in KNOWN_LIMITATIONS.md are pinned to each other
  • test/integration/graph-unsupported-languages.test.js: 35 pass (the "not tracked" answer now applies to Swift)
  • Full integration suite: 261 passed, 0 failed (macOS); the three test files of this PR also pass in a Linux container (Node 18, Swift 6.1)
  • validate:retrieval PASS (hard 63/90 at the 70.0% floor, see above); validate:xrepo PASS (116/208, no task changed)
  • check:metrics, check:version-meta, validate:llms, check:bundle:repro, VitePress build (no dead links)
  • Supply-chain gate: no shell spawns, no install scripts, main exports the API, no fingerprinting; nothing under scripts/, benchmarks/, test/ or docs-vp/ is in the tarball

🤖 Generated with Claude Code

…cript imports (refs #980)

The import graph read 8 language families; a blast-radius query on a file in any
other answered 'not tracked' (#979). It now reads 15. One resolver per family in
src/graph/resolvers, zero dependencies, precision first: an import that cannot be
resolved to exactly one file of the repository yields no edge (an external
package, a system header, a commented-out import, an ambiguous name).

C/C++/ObjC includes through the compiler's own search order; PHP use through
composer psr-4 (every matching prefix, every directory) then declared classes;
C# using linked to the files declaring the types the file names, not to the whole
namespace; Dart relative and package: imports; Vue, Svelte and Astro scripts
through the JS/TS resolver; Lua require; GDScript preload/extends. Laravel's
Str.php lists 164 importers, the same files grep finds.

Swift names modules, not files, so it draws no file edge: --impact on a Swift
file keeps 'not tracked' and lists the files in other SwiftPM targets that import
its module.

The ranker is still handed the original eight families (#934): buildRankingGraph
reads families 'legacy', so retrieval does not move with a better blast-radius
graph. Adding the new files to the repo's own corpus did move one hard task by a
BM25 margin of 0.012; folding resolvers/index.js into builder.js restored 63/90.
The #979 tests now use Swift for the file-level assertions.
…-only code stays out of the package (refs #980)

The seven resolvers added in a9dadaf live in src/graph/import-resolvers.js
instead of a directory of nine files, and the Swift module view is
src/graph/swift-modules.js. Behaviour is unchanged: the same fixtures, the
same 45 mutants killed (one more fixture closes a survivor: an alias of a type
the file declares itself must not be an edge to itself).

moduleEdges and swiftFiles' companion enumeration existed only for the
accuracy benchmark; the benchmark now builds that list from the product's own
moduleImpact (scripts/lib/import-accuracy.mjs), so the published package
carries only what a user surface calls.

Retrieval gate, measured against develop in a clean worktree: hard 63/90
(70.0%, the floor, 0 tasks of headroom), mined 66.7%, easy 90.0%, all
identical to develop, with no task changing between hit and miss. 27 of 166
task ranks moved by a place or two, which is what an added indexed file does
to the self-scored corpus; two earlier layouts of the same code flipped h089
and then h081. That is footprint, not design, and the gate has no headroom to
absorb it.
…efs #980)

A development run of the component reference on vitepress found that
`import {\n  a,\n  b\n} from '../x'` gave no edge: the matcher shared with
plain JS/TS is line-based. For Vue, Svelte and Astro scripts the matcher now
takes a list across lines, which is what prettier-formatted component code
writes.

Plain .js/.ts files keep the one-line reading, and so does the ranker's graph:
it is built from the same matcher, so widening it moves retrieval (#934) and is
its own change, with its own gate review. A test pins both halves (components
read, plain TypeScript and the ranking graph unchanged) and two mutants guard
them. KNOWN_LIMITATIONS says so, and names the two other specifiers neither
family finds: an extension-less one that resolves to a .d.ts, and ./x.js
written for an x.ts.

Retrieval gate on this tree: hard 63/90 (70.0%), mined 68.3%, easy 90.0%, no
hard task changes between hit and miss against develop; one mined task moves
from rank 6 to 5, which is the footprint of an indexed file, not a ranking
change.
…harness before the first run (refs #980)

How the import graph's file-to-file edges are scored against what each
language's own tooling reports, written down and committed before the scorer
has been run on any evaluation repository:

- references: clang -E -H on three configurations (C/C++/ObjC); the
  framework parser + ts.preProcessFile + ts.resolveModuleName (Vue, Svelte,
  Astro); PHP's tokenizer + Composer's own ClassLoader; swiftc
  -emit-imported-modules + swift package describe (Swift, module level).
- C#, Dart, Lua, GDScript are listed unmeasured, with the reason: no reference
  that could be run here. Their resolvers are covered by fixtures.
- evaluation repositories pinned to the commits the xrepo manifest pins;
  development repositories (spdlog, vitepress, guzzle, fixtures) kept out.
- eight predictions (P1-P8) with thresholds, and the cluster bootstrap of the
  call-graph benchmark (2000 resamples, seed 980, 95%); a cell under 100
  edges is marked and not quoted.
- known biases stated up front: PHP precision is a lower bound (literal
  require/include is not read by the reference), component recall is bounded by
  the shared JS resolver (.d.ts targets, ./x.js for x.ts, multi-line lists in
  plain JS/TS).

The harness (scripts/run-import-accuracy.mjs) records the protocol's sha256
and refuses --save for a partial run, an edited protocol or a dirty scored
tree. 25 mutants of the arithmetic, the page renderer, the adapters and the
runner's guards are killed by test/integration/import-accuracy.test.js.
Development runs on spdlog, vitepress and guzzle changed three things in the
references and removed one dead step; they are listed in the protocol.
…e second run (refs #980)

The first registered run reported how many edges were missed, not why. Reading
the component group's misses showed most of them share one cause (388 of
svelte's 390 are one target reached by a package naming itself), so the scorer
now counts, for every missed and every extra edge, how the import reaches its
file: written .js for a .ts file, a #name import, a JSON module, a bare
specifier that resolves inside the repository, a relative path to a .d.ts, a
file importing itself, else the extension of the file reached.

Nothing that decides an edge or a figure changed. The deviation log in the
protocol records the first run's headline figures, which the second run must
reproduce before anything is published. Six mutants of the new code are killed
by the harness test; the page renderer gains the two tables.
…ive component is not its own importer (refs #980)

Both found by reading the misses of the first registered run of the component
reference, both in the new code, neither a tuned rule:

- Astro accepts blank lines before the `---` fence; the pattern required the
  fence at the first character, so a file that starts with a newline lost its
  frontmatter (and with it every import).
- A Svelte or Astro component that renders itself (svelte:self, a recursive
  component) imports its own file. The C, C# and other new resolvers never
  draw a file's edge to itself; the component path did.

Tests pin both (and that a fence further down the file is not frontmatter);
two mutants are killed. Retrieval gate on this tree: hard 63/90 (70.0%), mined
68.3%, easy 90.0%, as before.
…e statement that PHP, C/C++ and components are unread (refs #980)

The registered run, from the committed protocol and a clean tree, over nine
pinned repositories (the report is the third run; the protocol's deviation log
keeps the first run's figures and says what changed):

  C / C++ / Objective-C    precision 99.7%  recall 99.7%   (clang -E -H)
  PHP                      precision 100.0% recall 100.0%  (Composer's autoloader)
  Swift, module level      precision 100.0% recall 99.3%   (swiftc + SwiftPM)
  Vue / Svelte / Astro     precision 100.0% recall 79.1%   (TypeScript resolution)

Seven of the eight registered predictions held. P4 (component recall >= 90%)
was missed, and the page says so: every remaining miss is in the resolver all
.js and .ts files share (a package importing itself by name through its
exports field, ./x.js written for x.ts, JSON modules, #name imports), which is
deliberately unchanged because the ranker's graph is built from it (#934).
C#, Dart, Lua and GDScript have fixtures and no figure: no reference could be
run.

The page's tables are generated from the saved report and pinned to it by a
test, as are the figures quoted in KNOWN_LIMITATIONS and the prose. cli.md
still said the import graph could not read PHP, C#, C/C++, Dart, Lua, GDScript
and the component languages, which stopped being true with a9dadaf; it now
lists what is read and what Swift gets.
…l spelling (refs #980)

Graph keys are lowercased. Dart's package discovery walked up from a KEY to look
for pubspec.yaml, so on a case-sensitive filesystem it looked in directories
that do not exist whenever a path had a capital letter, and every
`package:` import of a package of the same repository resolved to nothing. macOS
is case-insensitive, so the tests passed there; CI on Linux caught it.

It now starts from the file's real path, as the PHP, C# and GDScript resolvers
do. A new test builds all four resolvers that read project files (pubspec.yaml,
composer.json, a .csproj, project.godot) with capitals in the directory names
and runs them with fs behaving as on a case-sensitive filesystem, so the mistake
is visible on macOS too; a mutant that goes back to the key is killed.
…out, and log the two Linux fixes (refs #980)

`swiftc -emit-imported-modules -o /dev/stdout` fails on Linux with "No such
device or address" when the child's stdout is a socket, as it is under Node's
spawnSync, so every file was reported unreadable and the test failed in CI. It
now writes the module list to a temporary file.

The Swift group was run again on macOS without --save and matches the saved
report exactly (figures, counts, samples, diagnostics), so the report stands.
The protocol's deviation log records this and the Dart fix before it.
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