Skip to content

fix(registry): stop external package imports binding to project homonyms - #1766

Open
Yyunozor wants to merge 6 commits into
DeusData:mainfrom
Yyunozor:fix/1355-external-import-shadow
Open

Yyunozor wants to merge 6 commits into
DeusData:mainfrom
Yyunozor:fix/1355-external-import-shadow

Conversation

@Yyunozor

Copy link
Copy Markdown
Contributor

Addresses the external-import sub-case of #1355: the caller's own source already says the callee is not a project symbol.

Complements #1386, which guards the Python side of the same issue with a generic-name list. This one keys on import evidence rather than on names and is not language-gated; the repro and tests here are TypeScript. Both land at the same two emission sites — happy to rebase onto #1386 whenever it lands.

Reproduction

src/queries.ts does import { eq, sql } from "drizzle-orm"; src/text-utils.ts exports unrelated local eq/sql helpers. On main (34d18ae):

buildQuery  eq   …src.text-utils.eq   unique_name
buildQuery  sql  …src.text-utils.sql  unique_name

Two fabricated CALLS edges, identical under CBM_INDEX_SINGLE_THREAD=1 and CBM_WORKERS=4. The TS-LSP is not at fault: normalize, imported relatively from the very module the guess picked, resolves via lsp_ts_import @ 0.95. It declines the external names; the textual fallback fires regardless.

Cause

drizzle-orm is outside the indexed tree, so cbm_pipeline_resolve_import_node returns NULL, no IMPORTS edge is written, and the import map has no key for eq. Strategies 1-2 miss and resolve_name_lookup binds by simple name. Confidence does not separate the cases: the fabricated edge reaches 0.75 when the file also imports from the target's module.

Fix

cbm_suppress_external_import_shadow() — a pure predicate beside the existing cbm_perl_* / cbm_tsjs_* / cbm_suppress_cross_language_* guards. It drops the edge only when the callee is a bare identifier, the strategy is a project-wide guess (suffix_match / unique_name / field_type_hint / fuzzy), the file imports that name from a non-relative specifier, and no import-map key binds it.

Relative specifiers are excluded on purpose: they name a path inside the tree, so a missing IMPORTS edge there is an in-project gap and the fallback may still be right.

Verification

  • scripts/test.sh: branch 7569 / 1 / 8, parent 7565 / 1 / 8. Delta is exactly the 4 new tests; the pre-existing failure (test_cli.c:8914) is identical on both sides.
  • Red on the parent: the new pipeline test fails with the buildQuery → eq edge present, on both resolvers.
  • npm workspaces, pnpm-workspace.yaml, tsconfig paths, barrel and ./x.js imports keep their edges; Java import static and Python from pkg import f are untouched.

Known limitation

A bare specifier that does name in-project code but fails to materialize an IMPORTS edge (the workspace case in #1732) now loses its same-name fallback edge instead of keeping it at reduced confidence. A natural follow-up would consult the package map first.

Copilot AI lite review requested due to automatic review settings August 20, 2026 15:19
@Yyunozor
Yyunozor requested a review from DeusData as a code owner August 20, 2026 15:19
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

Copilot AI 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.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds a registry-level suppression guard to prevent project-wide fallback resolution from creating fabricated CALLS edges when the caller explicitly imports a homonymous symbol from an external (non-indexed) package.

Changes:

  • Introduces cbm_suppress_external_import_shadow() to suppress fallback-resolved edges for bare identifiers that are imported from non-relative specifiers but do not bind in the import map.
  • Applies the guard consistently in both sequential (pass_calls.c) and parallel (pass_parallel.c) emission paths.
  • Adds focused unit + pipeline tests reproducing #1355 and validating behavior for sequential vs parallel indexing.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_registry.c Adds unit tests for the new suppression predicate’s contract and edge cases.
tests/test_pipeline.c Adds an end-to-end fixture + assertions to verify the regression is fixed in both sequential and parallel resolvers.
src/pipeline/registry.c Implements cbm_suppress_external_import_shadow() plus helpers.
src/pipeline/pipeline.h Exposes the new suppression predicate in the public pipeline header.
src/pipeline/pass_parallel.c Invokes the guard in the parallel call-resolution emission path.
src/pipeline/pass_calls.c Threads file_imports into resolution and invokes the guard in the sequential emission path.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/test_pipeline.c Outdated
Comment on lines +4744 to +4746
char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
Comment thread tests/test_pipeline.c Outdated
Comment on lines +4739 to +4742
/* Enough files that CBM_WORKERS can take the fused-parallel path; the same
* tree is then indexed by each resolver in turn, because the guard lives at
* two independent emit sites (pass_calls.c and pass_parallel.c). */
write_external_import_shadow_fixture(tmp, 50);
Comment thread src/pipeline/registry.c Outdated
Comment on lines +460 to +463
* "/abs/x") rather than an external package. Package specifiers are everything
* else — "drizzle-orm", "rxjs/operators", "@scope/pkg". */
static bool specifier_is_relative(const char *module_path) {
return module_path && (module_path[0] == '.' || module_path[0] == '/');
@DeusData DeusData added bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges maintainer-notes Internal observations and improvement notes priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Aug 24, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thank you for the detailed reproduction, the sequential and parallel coverage, and for documenting the known workspace tradeoff explicitly. This changes project-wide import-resolution behavior, so we are reviewing the approach carefully and will come back with a maintainer decision before asking you to rework anything. Our review queue is currently full, so this may take a little time. Thank you for your patience and for making the tradeoff visible.

@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Reviewed. The predicate is well built, the reproduction is convincing, and there is a standing constraint on this class of change that decides the timing rather than the merit.

The predicate itself

I traced it, and the conjunction is tight in the right way: strategy limited to the four project-wide guesses, callee must be a bare identifier (no ., no ::), the file must have imports, the import map must not bind the name, and only then the external-specifier check. same_module, import_map, qualified_suffix, callee_suffix and every lsp_* strategy are untouched — which is the property that matters most here, because a blocked high-confidence match does not vanish, it demotes.

Excluding relative specifiers on purpose is the right line. A relative path names something inside the tree, so a missing IMPORTS edge there is an in-project gap where the fallback may still be correct. Getting that distinction the wrong way round would have been an easy and quiet mistake.

And the reproduction earns its conclusion. drizzle-orm's eq/sql binding to unrelated local helpers, identical under CBM_INDEX_SINGLE_THREAD=1 and CBM_WORKERS=4, with normalize from the very same module resolving correctly at 0.95 via lsp_ts_import — that last detail is what rules out "the TS-LSP is broken" and isolates the textual fallback. Confidence reaching 0.75 on a fabricated edge closes off the threshold answer too.

What holds it up

This PR is one of a named cluster — #1128, #1324, #1386, #1702 and this one — and there is a standing decision that they are judged against one shared census rather than one at a time, because whichever lands first shifts the baseline the others are measured on. You already spotted the nearest neighbour yourself: #1386 guards the Python side of the same issue and lands at the same two emission sites.

So this is a sequencing constraint, not a verdict.

Concretely, what would clear it. A change of this shape was accepted recently on four things together, and yours currently shows two of them:

  1. Upstream strategies provably untouched — you have this, and I verified it.
  2. Confidence demonstrably cannot substitute — you have this, with the 0.75 figure.
  3. A measured, per-language blast radius on real graphs — not yet. Edge counts before and after on a few real repositories, split by language, with the removals read by hand, is what turns "this removes fabricated edges" into a number someone can act on.
  4. No per-language seam available — this is the one I would push on. You apply the predicate at pass_calls.c and pass_parallel.c, where the file language is known, so a gate is available and you chose not to use it. Your reason is a real one — the gate is import evidence rather than a name list, and evidence generalises — but it is worth arguing explicitly rather than in passing, because the default here is per-language.

One interaction to check while you are at it: you drop field_type_hint, and #1907 deliberately keeps it for Go on the grounds that a Go struct field carries a declared type, so the hint is receiver-aware there rather than heuristic. I think your conjunction spares Go in practice — Go imports packages, not bare symbols, so the external-binding test should not fire — but that is my reasoning, not a measurement, and it belongs in your census.

Two smaller things

Your known limitation is real and honestly stated. A bare specifier that does name in-project code but fails to materialise an IMPORTS edge — the workspace case in #1732 — now loses its fallback rather than keeping it at reduced confidence. Consulting the package map first, as you suggest, is the right follow-up shape.

Your test / test-msan red is not yours. That job failed building its Docker image: apt-get install clang-22 from apt.llvm.org returned exit 100. No test ran. I have seen the same image build fail on an unrelated PR today, so it is a property of that lane.

A bare call whose name the calling file imports from a package outside the
indexed tree was bound to whatever project symbol shared the simple name.
An `import { eq, sql } from "drizzle-orm"` plus an unrelated local module
exporting `eq`/`sql` produced two CALLS edges from the caller into that
module, strategy unique_name — the same defect on the sequential and the
fused-parallel resolver. The TS-LSP is not involved: it declines the
external names, and the textual registry fallback fires anyway.

The package materializes no node, so resolve_import_node returns NULL, no
IMPORTS edge is written, and the per-file import map carries no key for the
name. Strategies 1-2 miss and resolve_name_lookup binds the call by simple
name. Confidence does not separate the cases: the fabricated edge lands at
0.75 whenever the file also imports anything from the target's module.

Add cbm_suppress_external_import_shadow(), a pure predicate beside the
existing perl/tsjs/cross-language guards and called at the same two emit
sites. It drops the edge only when the callee is a bare identifier, the
strategy is a project-wide guess (suffix_match / unique_name /
field_type_hint / fuzzy), the file imports that exact local name from a
non-relative specifier, and no import-map key binds it. Relative specifiers
are excluded on purpose: they name a path inside the tree, so a missing
IMPORTS edge there is an in-project resolution gap and the fallback can
still be right. A name imported both relatively and from a package keeps
its edge, independently of extraction order.

Addresses the external-import sub-case of DeusData#1355.

Signed-off-by: Yyunozor <yyunozor@icloud.com>
- specifier_is_relative() now also recognizes Windows drive-letter
  (C:\ / C:/) and UNC (\\server\share) specifiers as in-tree, not
  external packages. pr-smoke runs this pipeline on Windows, and the
  previous POSIX-only check ('.' / '/') classified those specifiers as
  external, which could suppress a real edge on that platform only.
  Added external_import_shadow_windows_relative_specifier_kept
  (tests/test_registry.c): red before this fix, green after.
- old_workers / old_single in the DeusData#1355 pipeline test are now
  const char*, matching getenv()'s read-only contract.
- The pad_files literal (50) is now a named
  EXTERNAL_IMPORT_SHADOW_PARALLEL_PAD constant with a comment tying it
  to MIN_FILES_FOR_PARALLEL (a private #define in pipeline.c, not
  reachable for a static_assert from tests), so a future bump to that
  threshold can't silently drop the fixture back onto the
  sequential-only path.

Signed-off-by: Yyunozor <yyunozor@icloud.com>
…ort guard

A per-language blast-radius census on twelve public repositories found one
regression class in the DeusData#1355 guard, and it is not per-language: it is
per-topology. In a workspace monorepo a BARE specifier can still name code
inside the indexed tree. `import { eq } from "drizzle-orm"` written inside
the drizzle-orm repository is a sibling package, not a dependency.

Measured on drizzle-team/drizzle-orm at b7862528, full index, both
resolvers: the guard removed 2609 CALLS edges. Resolving each removed
edge's specifier against the repository's own package manifests puts 1377
of them (52.8%) on the file the specifier actually names. 690 of those are
strict: the specifier names a subpath, that subpath maps to one directory,
and the target file is inside it. The other 687 come from the bare package
root, where the check can only confirm the target is somewhere in the
package, so read 690 as the defensible floor. Only 373 removed edges
pointed at a third-party package, which is the defect DeusData#1355 reports.

The same census on eleven other repositories (flask, scrapy, express,
zustand, got, cobra, gin, chi, ripgrep, gson, jq) removed 12 CALLS edges in
total: nine fabricated, one lost (a documentation example importing the
repository's own published name), two weak links between a shell command
and a manifest key. The regression is specific to trees that ship the
package they import.

Consult the pipeline package map before calling a specifier external. It is
keyed by the `name` of every manifest found in the tree, so a workspace
registers each of its own packages there, and a subpath specifier is walked
back one slash at a time ("drizzle-orm/pg-core" -> "drizzle-orm"). A
specifier the tree itself claims is now treated exactly like a relative
one -- kept.

The check is deliberately a NAME test rather than a resolution test. A
workspace package usually points `main` at a build artifact ("./index.cjs")
that is not checked in, so asking whether the entry file exists in the
graph answers "no" for exactly the monorepos this has to protect. Whether
the tree claims the name is decidable from the manifest alone. Passing NULL
restores the previous specifier-shape-only contract, which is what the unit
tests exercise.

After the change the same census removes 391 edges on drizzle-orm, 375 of
them CALLS, and every one resolves to a third-party package; of the other
eleven repositories only got changes, recovering its self-referencing edge.

Signed-off-by: Yyunozor <yyunozor@icloud.com>
@Yyunozor
Yyunozor force-pushed the fix/1355-external-import-shadow branch from f78bfe2 to 797a11a Compare September 4, 2026 21:56
@Yyunozor

Yyunozor commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the review. I ran the census. It found a regression class in my
own patch, so I will start with that.

Summary

  • Twelve public repositories, eight languages, indexed with main (2f9828d6) and this branch. Go, Rust, Java and C are byte-identical; a traced binary shows the predicate never fires there.
  • One regression class, in TypeScript workspaces. On drizzle-orm the published patch removed 2609 CALLS edges, and 690 of them (1377 by a looser reading) were real in-project links: inside a monorepo, import { eq } from "drizzle-orm" names a workspace sibling, not a dependency.
  • The fix, in the new commit, consults cbm_pipeline_get_pkgmap() before calling a specifier external. That is the follow-up shape you named, moved into the fix. drizzle-orm now removes 375 edges, all third-party, 0 regressions; the other eleven repositories are unchanged.
  • The blast radius is per topology, not per language. A per-language gate would have caught nothing. Caveat: one TypeScript workspace measured.
  • fix(pipeline): suppress weak short-name matches for Go selector calls #1907, measured on cobra, gin and chi: #1907 alone and #1907 + this PR produce byte-identical graphs, and every field_type_hint edge in main survives in all four builds.
  • Tests: registry pipeline 337 passed / 0 failed (parent 330 / 0, delta = the 7 added tests). Full suite: 7821 passed / 1 failed, and that failure is identical on the bare parent.
  • Rebased onto 2f9828d6 (106 commits). If you would rather judge the cluster against the version you already read, say so and I will split the package-map commit out.
Full census: per-repository tables, edges read by hand, the #1907 fixture, caveats

3. Measured blast radius, per language

Twelve public repositories, one index with main (2f9828d6) and one with this
branch, both binaries built from source. Edge counts come from the produced
SQLite graph.

language repo before after delta CALLS removed
Python pallets/flask 7420 7419 -1 1
Python scrapy/scrapy 47943 47942 -1 1
TypeScript (pnpm workspace) drizzle-team/drizzle-orm 54338 51723 -2615 2609
TypeScript (single package) pmndrs/zustand 1803 1800 -3 3
TypeScript (single package) sindresorhus/got 5314 5310 -4 4
JavaScript expressjs/express 2169 2166 -3 3
Go spf13/cobra 6065 6065 0 0
Go gin-gonic/gin 12389 12389 0 0
Go go-chi/chi 5043 5043 0 0
Rust (cargo workspace) BurntSushi/ripgrep 24035 24035 0 0
Java google/gson 42207 42207 0 0
C jqlang/jq 7569 7569 0 0

Go, Rust, Java and C are byte-identical before and after. I also built a traced
binary that logs every time the predicate fires. On those four languages it
fires zero times.

drizzle-orm is the problem. I classified all 2609 removed CALLS edges
mechanically: read the import statement in the source file, resolve the
specifier against the repository's own package manifests, compare to the target
file.

verdict n
regression — target IS the file the specifier names 1377 (52.8%)
correction — in-tree package, but wrong sibling file 855
correction — real third-party package (the #1355 bug) 373
import not found (.cjs require()) 4

The 1377 are not all equally strong evidence, so here is the split. In 690 the
specifier names a subpath (drizzle-orm/pg-core), which maps to one directory
(drizzle-orm/src/pg-core/), and the target file sits inside it — a strict
match. In the other 687 the specifier is the bare package root
(drizzle-orm), so the only thing I can check is that the target sits inside
that package; I did not resolve the root's re-exports, so treat that half as
the weaker evidence.

I read 14 of them by hand. pgTable imported from drizzle-orm/pg-core was
bound to drizzle-orm/src/pg-core/table.ts. That is the correct definition, and
my guard removes it. Same for sqliteTable, mysqlTable, varchar,
interval, singlestoreEnum, sql, eq, gt, like, relations,
hammingDistance. The reason is simple: inside the drizzle-orm repository,
import { eq } from "drizzle-orm" names a workspace sibling, not a dependency.

The other eleven repositories removed 12 CALLS edges in total: express 3,
zustand 3, got 4, flask 1, scrapy 1, and zero everywhere else. I read all 12.

  • 9 fabricated. express: createError from http-errors twice, Router
    from router once. zustand: render from @testing-library/react three
    times, all bound to a local ErrorBoundary.render. got: delay from
    node:timers/promises three times.
  • 1 loss. got: got imported from 'got' in a documentation example. That
    is the repository's own published name, so it is the same class as drizzle.
  • 2 debatable. flask and scrapy shell scripts where a CLI tool name was
    linked to a manifest key (pre-commit -> pyproject.toml, coverage ->
    codecov.yml). Weak in both directions.

9 + 1 + 2 = 12.

4. Why there is no per-language gate

The census answers this better than an argument would. The blast radius is not
per language. It is per topology.
The one regression class lives in
TypeScript, which is the language the fix targets. A is_typescript gate would
have caught nothing.

What separates the safe repositories from drizzle-orm is not the language. It is
whether the import map binds. scrapy writes from scrapy.utils.job import job_dir — absolute self-imports, exactly the risky shape — and the import map
binds them, so the predicate returns early and never reaches the specifier test.
drizzle-orm's map does not bind, because drizzle-orm/package.json points
main at ./index.cjs, a build artifact that is not checked in.

So the correct seam is the package map, which is what you called the right
follow-up shape. I moved it into the fix rather than leaving it as a follow-up,
because 690 lost edges on the strict half alone is not a limitation, it is a
defect.

The new commit consults cbm_pipeline_get_pkgmap() before calling a specifier
external. The map is keyed by the name of every manifest in the tree, so a
workspace registers each of its own packages; a subpath is walked back one slash
at a time (drizzle-orm/pg-core -> drizzle-orm). It is deliberately a name
test and not a resolution test, because the declared entry file is usually not
in the repository — an entry lookup answers "no" for exactly the monorepos this
has to protect. Passing NULL restores the previous contract, which is what the
unit tests use.

Same census after the change:

repo before after delta CALLS removed regressions
drizzle-orm 54338 53947 -391 2609 -> 375 1377 -> 0
got 5314 5311 -3 4 -> 3 1 -> 0
express, zustand, flask, scrapy unchanged (3, 3, 1, 1) 0
cobra, gin, chi, ripgrep, gson, jq 0 0 0

got is the only one of the eleven that changes: the self-referencing got edge
comes back. flask's net delta reads 0 rather than -1, but that is not the guard
— its one CALLS removal stands, and an unrelated HANDLES edge reappeared
through the upstream drift described at the end.

All 375 resolve to a third-party package. I read 10: test from vitest,
union from zod, Database from better-sqlite3, createPool from
mysql2, beforeEach from vitest. The 2 "import not found" are CommonJS
require('better-sqlite3') and require('postgres'), so also third-party.

Tests: scripts/test.sh --suites 'registry pipeline' gives 337 passed 0 failed
on the branch and 330 passed 0 failed on bare 2f9828d6. Delta is exactly the 7
tests added.

Each emission site has its own red proof. Replacing cbm_pipeline_get_pkgmap()
with NULL in pass_calls.c alone fails on sequential == 4; doing it in
pass_parallel.c alone fails on parallel == 4; doing both fails on the
sequential assertion first. 336 passed 1 failed in each case, and after each one
both files are restored from the commit and checked by sha256. clang-format
22.1.4 reports the same replacement counts on branch and parent for all six
files.

The #1907 interaction

Your reasoning holds, and here is the measurement. Numbers below are from my
run — please read the absolute totals as such, for a reason I give right after
the table.

repo main this PR #1907 both
cobra 6065 6065 6056 6056
gin 12389 12389 12030 12030
chi 5043 5043 4973 4973

The absolute totals do not travel between indexing sessions. Four fresh
indexations of cobra and chi with the same main binary on my machine agree
exactly (chi 5043 edges / 1456 CALLS / 122 field_type_hint, four times out of
four). An independent replay in another session got chi 5132 / 1521 / 118 and
cobra 6135, with the difference concentrated in the LSP-resolved strategies
(lsp_type_dispatch and unresolved calls). So the counts are stable within a
session and not reproducible across sessions, and I do not know why.

What does hold in both sessions is the comparison, which is what the question
is about. Within one session: main and this PR are identical at the sha256 of
the whole edge set on all three repositories, and #1907 and #1907 + this PR
are identical to each other. And every field_type_hint edge present in main
is still present in all four builds — 122 in chi and 13 in gin in my run, 118
in chi in the other, unchanged in each case.

So my drop-list naming field_type_hint never reaches a Go edge. The
conjunction fails earlier: a Go field_type_hint match is a selector call, and
a Go import binds a package identifier, not a member name. Zero dot-imports in
the 219 Go files measured.

The seam does exist. I built a fixture with import "github.com/unindexed/render"
plus a bare render("hello") call and a project function render. main
produces the fabricated edge, this PR removes it, #1907 leaves it (the call is
bare, so is_method is false), both together behave like this PR. That shape is
not valid Go — the identifier would be both a package and a function — but if it
ever appears, the guard fires in the right direction.

Two notes

The branch was 106 commits behind, so I rebased it onto 2f9828d6. Upstream had
added cbm_suppress_weak_local_binding_call at the same anchors; both sides are
kept.

The known limitation is now narrower but still real. A bare specifier that names
in-tree code with no manifest declaring that name — an unusual bundler alias,
for instance — still loses its fallback. The package map does not see it.

The "per topology, not per language" claim rests on one workspace. drizzle-orm
is the only monorepo of that shape in the census. ripgrep is a cargo workspace
and gson is a multi-module Maven build, and neither fired — but the reason is
call shape (Type::method, Type.method are not bare identifiers), not proof
that the topology is safe there. A Rust repository with many
use crate::...::{function} imports plus bare calls would be the test I have
not run.

Rebasing this changes the diff you already traced. If you would rather judge the
cluster against the version you read, say so and I will split the package-map
commit out.

Repository SHAs used: drizzle-orm b7862528, zustand b57db4f8, got
687eb7dc, express 023767fe, flask d318b683, scrapy ebfb0491, cobra
adbc8813, gin dcaa4296, chi ae6be746, ripgrep 3fce3b5b, gson
b3f4ca20, jq 9d241e27.

Measurement caveats. Running the same main binary twice on flask differs by 21
lines, all HANDLES edges to __route__ANY__/. Separately, the Go absolutes
move between indexing sessions as described above, in the LSP-resolved
strategies. Both drifts are upstream and unrelated to this diff, but together
they mean absolute edge counts in this comment should be read as one run's
figures, not as constants; the before/after comparisons within a session are the
part that reproduced. Full suite on this branch: 7821 passed, 1 failed, 7 skipped, 141/141 suites;
bare parent 2f9828d6 on the same machine: 7813 passed, 2 failed. The delta is
exactly the 7 tests this branch adds. The one branch failure is
tests/test_cli.c:9502 (VS Code-only install detection) and it fails the same
way on the bare parent, so it is this machine, not the diff. The parent's second
failure is the test_mcp.c:12540 timing flake (4 of 5 parent runs, 1 of 4 branch
runs).

@Yyunozor

Yyunozor commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

One correction to my last note: I have since run the full suite on this branch (797a11a1), not only registry pipeline.

Branch: 7821 passed, 1 failed, 7 skipped, 141/141 suites. Bare parent 2f9828d6 on the same machine: 7813 passed, 2 failed. The delta is exactly the 7 tests this branch adds.

The one failure on the branch is tests/test_cli.c:9502 (VS Code-only install detection). It fails the same way on the bare parent, so it is this machine, not the diff. The parent's second failure is the test_mcp.c:12540 timing flake; it failed in 4 of 5 parent runs and 1 of 4 branch runs, so it is not related to the diff either.

@DeusData

DeusData commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Census done — the third item on the bar I gave you, measured per-language blast radius on real graphs (ten languages, ~3.1M CALLS edges, every removed edge classified). The JS/TS/Java/PHP effect is correct and valuable: koel's import { ref } from 'vue' bound to a spec file's local ref 185×, elasticsearch's java.net.URL bound to an in-tree URL class 113×, tslib's __classPrivateFieldSet bound to a local helper — all gone, all rightly. It also composes exactly with #1702 (removed(both) = removed(#1766) ∪ removed(#1702) on nine corpora, zero additions). Two things before it merges:

  1. JVM package imports are read as external. kotlin −398 CALLS, 100% true in-tree calls: import org.jetbrains.exposed.v1.tests.shared.assertEquals → the project's own Assert.kt (InsertTests.testInsertOfDefaultValuesOnlyAssert.kt.assertEquals, EncryptedColumnTestsAssert.assertTrue, 34× and more). Kotlin/Java specifiers are package paths, never "relative", and the package map only knows package.json / go.mod / Cargo.toml names, so the guard cannot tell in-tree from external there. Either resolve package-path specifiers against the project's own declared packages before calling them external, or scope the guard to the specifier-shape languages (JS/TS/Python) where the evidence holds. Either way this becomes per-language by evidence — the project's standing rule for call-resolution suppressors, and the fourth item on the bar.
  2. The guard continues before route classification. java loses 88 HANDLES (infra_match) edges and 5 Route nodes, which the PR text says are untouched. On inspection those edges were spurious, so there is no quality loss — but either the description should say so, or the guard should run after route classification so the PR does what it claims.

Same sign-off, then merge on green. Thanks for the careful predicate work — the drizzle-orm repro is what made the census worth running.

… packages

JVM package imports were read as external. A Kotlin or Java specifier is a
dotted package path whether the package belongs to the indexed tree or to a
dependency -- `import org.jetbrains.exposed.v1.tests.shared.assertEquals`
and `import kotlin.test.assertEquals` have the same shape, and neither is
"relative". The package map cannot separate them either: the manifests it
reads for the JVM (pom.xml, build.gradle) name build artifacts, not
packages. So every package-path specifier fell through to "external" and
the guard cut calls into the project's own code.

Measured on JetBrains/Exposed at 0e4d81a5, full index: the guard removed
426 CALLS edges, all of them suffix_match, all of them into the
repository's own exposed-tests/.../shared/Assert.kt. Every callee was
assertEquals, assertTrue or assertFalse -- names that file declares and
that the test files import twice, once from the project package and once
from kotlin.test. The double import is what leaves the import map without a
key and hands the call to the project-wide guess.

The evidence that separates the two is the tree's own `package`
declarations, and the pipeline already collects them: the import passes
build a namespace map keyed by the dot-normalized `package` / `namespace` /
`use` of every indexed file, to resolve namespace imports. Publish that map
for the duration of the run instead of freeing it at the end of the import
pass, and consult it before calling a package-path specifier external. The
trailing member segment is walked off one at a time, the same way the
namespace-map lookup in cbm_pipeline_resolve_import_node does it.

This keeps the effect the census called valuable. `java.net.URL` walks to
"java.net" and then "java", neither of which any project file declares, so
a bare `URL` bound to an in-tree URL class is still cut. Only specifiers
the tree itself claims are kept. Passing NULL restores the previous
contract, which is what the unit tests exercise.

After the change Exposed carries 31195 CALLS edges, the same as the
merge-base, with none removed and none added -- stable across 28 indexations
of the repository (8 with this branch, 20 single-threaded). Total edge counts
are not a usable invariant here: roughly one indexation in ten materializes a
`.properties` resource file as a File node under Folder nodes instead of a
Module with its Variables, on the merge-base binary as well, and the file it
picks varies. That flip never touches a CALLS edge and disappears entirely
under CBM_INDEX_SINGLE_THREAD=1.

The walk that strips the trailing member segment has no floor, on purpose: a
third-party package published under a prefix the tree declares reads as
in-tree and the guard stands down, leaving exactly the edge the merge-base
emits. A call-resolution suppressor has to fail on that side.

Signed-off-by: Yyunozor <yyunozor@icloud.com>
The DeusData#1355 guard dropped the call before the emitters ran, so it did not
only remove the fabricated CALLS edge it is meant to remove: it also
skipped the route, HTTP, CONFIG and URL classification that happens inside
emit_classified_edge / emit_service_edge. A PR that claims to touch nothing
but weak same-name CALLS edges was silently costing Route nodes, and with
them every HANDLES edge that pass_route_nodes.c later bridges onto those
routes.

The shape is not hypothetical. A route registration whose router is a
dependency is exactly a bare call bound by a package specifier: the
resolved QN carries the router library name, emit_classified_edge reads it
as CBM_SVC_ROUTE_REG, and the first argument is path-shaped. On a
66-file TypeScript fixture main mints __route__ANY__/orders and the guard
removed it, on both resolvers.

The fix is the seam the DeusData#592/DeusData#606 member guard already uses, for the same
reason and with the same comment: join the predicate to drop_plain_call and
let the emitter decide. Only the plain-CALLS fall-through is suppressed, so
the fabricated edge still goes and every service edge stays main-identical.
The two resolvers keep identical gates, as the notes in both files require.

The new pipeline test asserts both halves on both resolvers: the Route node
survives, and a second bare call from the same external package in the same
file is still cut, so the first assertion cannot pass by the guard having
been disarmed.

Signed-off-by: Yyunozor <yyunozor@icloud.com>
@Yyunozor

Copy link
Copy Markdown
Contributor Author

Both items are fixed, in two commits. Numbers below are from my run. The
comparison binary throughout is the merge-base 2f9828d6, not current main:
this branch is not rebased, origin/main is 59a05eb1, 299 commits above
the base, and the PR still reports MERGEABLE. Say the word if you want it
rebased before it goes in.

Summary

  • JVM imports. I reproduced your Kotlin finding: on JetBrains/Exposed 0e4d81a5 the published guard removes 426 CALLS edges (you measured 398). All 426 are suffix_match, all target a .kt file in the tree, and the callees are only assertEquals (378), assertTrue (42) and assertFalse (6) — the project's own Assert.kt. After the fix Exposed carries 31195 CALLS edges, the same as the merge-base, none removed and none added, in 28 out of 28 indexations.
  • I took your first option, not the second. The pipeline already collects the project's declared packages: cbm_pipeline_namespace_map_build keys a table by the package / namespace / use declaration of every indexed file, and the import passes already use it. It was freed at the end of the import pass; now it is published for the run, like the package map, and the guard consults it. The member segment is walked off one dot at a time, the same way cbm_pipeline_resolve_import_node does it.
  • The walk is permissive on purpose, and I would rather say so than let you find it. It has no floor, so once the tree declares com.example, a third-party library publishing under com.example.vendor also reads as in-tree and the guard stands down. That leaves a fabricated edge the guard could have removed; it never removes a real one, which is the side a call-resolution suppressor has to fail on. There is a unit test pinning it. If you want a floor — stop the walk at the first type-shaped segment, say — I would rather you pick the rule than guess it.
  • Third-party packages that share no declared prefix are unaffected: java.net.URL walks to java.net then java, neither declared by any project file, so a bare URL bound to an in-tree URL class is still removed. I did not index elasticsearch or koel — I ran out of disk — so the effect you called valuable is preserved by the mechanism and by gson and petclinic firing nothing, not by re-measuring your two repositories.
  • Route classification. You were right, and the cause is not the one I would have guessed. The guard can never match a route callee by name (route_reg_suffixes all start with . or ::, and the guard only fires on bare callees). It is the resolved-QN path: cbm_service_pattern_match does a substring match on the QN, so a bare call resolving to a project symbol whose QN contains express, flask, gin. and so on lands in the ROUTE_REG branch — and the continue dropped it before the emitter.
  • The fix is the seam the task: Parsing & extraction quality — language/format coverage gaps #592/[bug] PHP: framework/core method calls ($storage->load()) become false-positive CALLS edges to same-named project methods — same class as the (fixed) Perl #476 #606 member guard already uses: join the predicate to drop_plain_call and let the emitter decide. Only the plain-CALLS fall-through is suppressed. On a 66-file fixture the merge-base mints __route__ANY__/orders, the published guard loses it, this branch keeps it and still cuts a second bare call from the same external package in the same file. Same result on both resolvers.
  • Census, 14 repositories, three binaries each. This branch is identical to the published head everywhere except Exposed, where it is identical to the merge-base. No new edge type anywhere.
  • Tests: registry pipeline is 341 passed / 0 failed here, 337 on the published head, 330 on the bare merge-base. The delta is exactly the 4 tests these commits add. Two of the seven code sites I touched have no red test — the sequential-path plumbing of the first commit; details below, including why and the CLI evidence that they are not dead code.
  • One correction to my earlier census: it used one repository per language, and that is how it missed this. gson changed zero edges, so Java looked safe, and I never indexed a JVM repository where the same name is imported from two paths — which is the condition that puts the call in front of this guard at all.
Detail: the Kotlin mechanism, the route mechanism, an indexing flip I had to chase, the census, the red proof, and what I could not measure

Why Kotlin, and why only three names

EncryptedColumnTests.kt imports assertEquals twice: from
org.jetbrains.exposed.v1.tests.shared on line 11 and from kotlin.test on
line 14. Assert.kt declares package org.jetbrains.exposed.v1.tests.shared.

pxc_kotlin_import_from_metadata goes through pxc_unique_import_path, which
reports ambiguous as soon as one local name comes from two module paths. No
key enters the import map, import_map_binds is false, and the guard then sees
a non-relative specifier the package map does not know. So the double import is
the entry condition, and that is why the 426 edges sit on three names rather
than spread across the repository.

Ten read by hand, all the same shape: EncryptedColumnTests.assertEquals ×4,
HashedColumnTests.assertTrue ×3, HashedColumnTests.assertFalse ×3, every one
targeting exposed-tests/src/main/kotlin/org/jetbrains/exposed/v1/tests/shared/Assert.kt.

An indexing flip, and why I stopped quoting total edge counts

My first draft of this comment said Exposed came out "byte-identical, 263895
edges". That sentence does not reproduce, and chasing it was worth the time.

Roughly one indexation in ten materializes a single .properties resource file
as a File node under Folder nodes instead of as a Module with its Variables:
−1 Module, −7 or −35 Variables, −8 or −36 DEFINES, +1 File, +1 or +2 Folders.
The file it picks varies between occurrences.

It is not this diff. Evidence, 58 indexations of Exposed, repository tree
verified clean with git status --porcelain --ignored before and after every
one of them:

binary runs outcome
merge-base 2f9828d6 30 29 majority, 1 flip (gradle-wrapper.properties)
this branch 8 7 majority, 1 flip (platform-map.properties)
this branch, CBM_INDEX_SINGLE_THREAD=1 20 20 majority, 0 flips
published head 10 10 stable

The majority state of this branch is byte-identical to the majority state of
the merge-base. The merge-base flips too, so the correlation with our binaries
that I first suspected was small-sample luck; it disappears entirely
single-threaded, so it is an ordering effect on the parallel path. And CALLS is
invariant across all 58 runs — 31195 on the merge-base and on this branch,
30769 on the published head — so it never touches the layer this PR changes.
That is why the claim above is stated in CALLS edges.

The route mechanism

route_reg_suffixes[] in internal/cbm/service_patterns.c contains .get,
.Post, ::delete, .HandleFunc and so on — every entry starts with . or
::. cbm_suppress_external_import_shadow returns false whenever the callee
contains . or ::. The two predicates cannot overlap, so the loss does not
come from cbm_service_pattern_route_method.

It comes from cbm_service_pattern_match(res->qualified_name), a strstr
against the library table. The resolved QN carries the project name and the file
path, so a bare call that suffix_match binds to a project symbol in, say,
express-routes.ts is classified ROUTE_REG. With a path-shaped first argument
the emitter mints a Route node; the old continue skipped that, and
pass_route_nodes.c then had nothing to bridge its infra_match HANDLES onto.

Fixture, 66 TypeScript files. app/express-routes.ts exports regGet and
regSpec; app/server.ts has import { regGet, regSpec } from 'vendor-router'
and calls regGet('/orders', () => 'ok') + regSpec('case').

binary Route nodes route edge for regGet fabricated edge for regSpec
merge-base 1 1 1
published head 0 0 0
this branch 1 1 0

Identical under CBM_INDEX_SINGLE_THREAD=1 and under the parallel resolver.

flask is not evidence for this, and I want to say so

The raw census shows the published head removing 22 HANDLES and adding 10 on
flask, which looks exactly like your finding. It is noise. Three indexations of
flask d73fa1cd with the same merge-base binary give three different
edge-set hashes and 81, 70 and 82 HANDLES. Two merge-base runs differ by
−21 / +10 HANDLES on their own. CALLS is the stable part: 1395 on the
merge-base, 1394 on both branches, one real removal. I did not use flask for
any HANDLES claim.

Census

Full index per repository per binary. "removed" means present in the merge-base
graph and absent in the variant. Four of these moved since my last comment, so
the SHAs are restated in full.

repo merge-base published head this branch
pallets/flask d73fa1cd 7472 CALLS 1, HANDLES 22/+10 ¹ CALLS 1
scrapy/scrapy fe30c188 49497 CALLS 1 CALLS 1
drizzle-team/drizzle-orm b7862528 54276 CALLS 375, SR 87/+96 CALLS 375, SR 87/+96
pmndrs/zustand b57db4f8 1803 CALLS 3 CALLS 3
sindresorhus/got 687eb7dc 5314 CALLS 3, SR 1 CALLS 3, SR 1
expressjs/express 9a34acf0 2192 CALLS 3 CALLS 3
spf13/cobra adbc8813 6115
gin-gonic/gin 3b08cd72 12943
go-chi/chi 3d1777a1 5150
BurntSushi/ripgrep 3fce3b5b 24146
google/gson 854c8255 42706
jqlang/jq df249813 7569
JetBrains/Exposed 0e4d81a5 263895 CALLS 426, SR 121/+113
spring-petclinic 818c4136 3249 ² ² identical to published head

¹ upstream noise, measured above.
² petclinic produces the same graph on both branches; all three binaries show
the .properties flip described above.

A dash means zero changed edges. For gson and the Go repositories I am
reporting that no edge changed, which is an observation; I did not re-run the
traced binary this round, so "the predicate never fires there" stays an
inference from it.

Red proof, one site at a time, on the committed state

Each site broken alone, rebuilt, suites run, restored from HEAD, sha256
checked before and after.

site broken file test that turned red
specifier_names_declared_package registry.c test_registry.c unit + test_pipeline.c parallel
namespace-map publication, parallel pass_parallel.c test_pipeline.c parallel
argument passed, parallel pass_parallel.c test_pipeline.c parallel
guard placement, sequential pass_calls.c route test, sequential == 4
guard placement, parallel pass_parallel.c route test, parallel == 4
namespace-map publication, sequential pass_definitions.c none
argument passed, sequential pass_calls.c none

The last two have no red test. In the test harness the project name comes from
the temp directory, and the sequential import-map builder (build_import_map in
pass_calls.c, a different function from its parallel twin
cbm_pxc_build_import_map) then binds both names by import_map — a strategy
this guard never touches. So the guard is not reached on that path for that
fixture.

They are not dead code. The same fixture indexed through the CLI with a short
project name and CBM_INDEX_SINGLE_THREAD=1 gives, from pkgBuild: merge-base
pkgEq + pkgSpec, published head nothing at all, this branch pkgEq only.
And the notes in both files require the two resolvers to carry identical gates
for MT determinism, so passing NULL on one side would be a deliberate
divergence.

Formatting checked with clang-format --output-replacements-xml, Homebrew
clang-format 22.1.4, comparing each touched file against its 2f9828d6
version: same replacement count on both sides for all ten files. TEST() and
RUN_TEST() match in both test files (70 and 301), and no test block has
unbalanced braces on either side.

Per-language by evidence

I did not gate the declared-package check by language in the code. It is a
restriction on the suppressor rather than a new suppression — it only keeps
edges — and the namespace map is empty for languages with no package
declaration. The evidence is the census: across 14 repositories this branch
differs from the published head only on Exposed. If you would rather see an
explicit gate on the JVM/CLR/PHP languages, the predicate does not currently
receive lang, so it would mean a new parameter, both call sites and the unit
tests that carry the contract. I have not written or measured that version.

What I could not measure

I did not find the Java repository behind your 88 HANDLES and 5 Route nodes.
petclinic never fires the guard and flask is too noisy on HANDLES to carry a
claim. So the route mechanism here is established from the code and from a
deterministic fixture, not from reproducing your corpus. elasticsearch and koel
are not indexed.

I ran registry pipeline for this round, not the full suite. Absolute edge
counts are one session's figures; the CALLS comparisons are the part that
reproduced.

@DeusData DeusData left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you — and I mean that with some weight. You took the census seriously enough to find a regression in your own patch, reported it first, and then fixed both of my findings with a mechanism rather than a special case. That is a rare way to work, and it is why this review went as deep as it did. Here is what I checked, what holds, and the one thing that does not yet.

What holds

Both 5 September items are really fixed on a full index, and the tests bind. I reverted the production hunks only, tests untouched:

  • 81820262 (guard runs after route classification — drop_plain_call instead of the early continue), both sites reverted → pipeline + registry: 355 passed, 1 failed
    FAIL tests/test_pipeline.c:6471: sequential == 4, expected 0 == 0
  • 138a6225 (declared-package set via the published namespace map), mechanism disabled at registry.c:816 → 4 failures, among them
    external_import_shadow_declared_package_kept FAIL tests/test_registry.c:1239
    FAIL tests/test_pipeline.c:6364: parallel == 4, expected 0 == 0
  • whole predicate disabled → 3 failures at test_pipeline.c:6127, :6237, :6471.

The predicate is cheap and safe in the ways this codebase has been burned before. No registry access at all; no lookup inside a registration loop; cost per resolved call is O(K + I·D) over this file's import-map keys and imports, and only after the cheap filters (weak strategy, bare callee, file has imports) — calls resolved by import_map / same_module / lsp_* leave after four strcmps. Zero allocations (one CBM_SZ_512 stack buffer, over-long specifiers return false). g_nsmap is written once before the resolve workers start and only read afterwards, the same single-writer contract as g_pkgmap, and it owns its keys, so extending its lifetime to the end of the run is safe under spill. The complexity suite is green on the merge result.

It merges onto today's main cleanly and the result is green: no textual conflict, builds with -Wall -Wextra -Werror under ASan+UBSan, complexity 5 · extraction 350 · registry 71 · pipeline 285 · cross_repo 8 · parallel 74 · lang_contract 41 · incremental 163 = 997 passed, 0 failed; the memory-core linter shows no growth from this PR. main's newer is_test_flags parameter lives in candidate ranking (best_by_import_distance), which you do not touch and your verdict does not depend on. (Your head is 21 commits behind main, not 300 — your own merge of 20 September brought it nearly up to date.)

The one blocking finding: the JVM fix does not survive an incremental re-index

g_nsmap is built by cbm_pipeline_namespace_map_build[_names] from the file list of the current pass. On a full index that is every file. On the incremental / closure_repair route it is changed_files only (pipeline_incremental.c:1254 and :1385 hand that list to cbm_build_registry_from_cache / cbm_pipeline_pass_definitions). So when someone edits only app/Query.kt, the file that declares package com.example.util is not in the map, specifier_names_declared_package answers "not declared", and the guard suppresses the real edge again — the Kotlin regression from 5 September, coming back through the watcher.

Reproduced on your own Kotlin fixture: full index, then rewrite only the caller file without changing a single name, re-run. With an attribution control (the predicate returning false at entry) to prove it is the guard and nothing else:

guard armed (this PR):  round=0 route=0 (full index)  true_edge=1 fabricated_edge=0
                        round=1 route=closure_repair true_edge=0 fabricated_edge=0   <- real edge LOST
guard disarmed:         round=0 route=0 (full index)  true_edge=1 fabricated_edge=1
                        round=1 route=closure_repair true_edge=1 fabricated_edge=1

(The fabricated_edge column is the good news: on a full index the guard does exactly what it promises.) The TypeScript-workspace side is immune — same probe, true_edge=1 in both rounds and both arms — because the pkgmap comes from cbm_pkgmap_build_from_repo, a tree-wide manifest walk that does not depend on which files changed.

Beyond the lost edges this makes the graph depend on how it was built — full and incremental disagree about the same source tree — which is a property we work hard to keep out of this project. On an Exposed-shaped repository every edited test file would lose its assertEquals / assertTrue edges until the next full index.

Fix shape, your choice: source the declared-package set tree-wide on the incremental routes too (seed it from the existing graph/store, or persist it next to the pkgmap), or have the guard stand down when the map is known to be partial. The second is the smaller change and errs in the safe direction (it keeps an edge main already has). Either way it needs a RED test on the closure_repair route — the probe above is that test almost verbatim (cbm_pipeline_incremental_test_last_route() gives you the route to assert on), and I am happy to paste it in full if useful.

Smaller things

  1. An inert assertion, which you already suspected. tests/test_pipeline.c:6363ASSERT_EQ(sequential, 0) in …keeps_declared_package_issue1355 — still passes with the mechanism disabled; only :6364 (parallel) goes red. Your explanation is right: the sequential build_import_map binds both names by import map under the temp-dir project name, so the guard is never reached. That leaves the sequential g_nsmap publication in pass_definitions.c and the nsmap argument in pass_calls.c without a red test. A fixture that actually reaches the sequential guard closes it.
  2. Say in the PR text what 81820262 decided. Of the two options I gave for the route-classification item you took "run after classification", which deliberately keeps the 88 Java HANDLES edges / 5 Route nodes I had called spurious — the branch is main-identical there by design. That is a defensible choice; it should be stated, so nobody later reads it as an oversight.
  3. The per-language question (my 1 September item 4) is still open, and it is mine to settle, not yours. The predicate takes no lang, and the namespace map is not language-scoped, so in a polyglot repository a Kotlin or PHP package name can make a JS bare specifier read as in-tree. It fails permissive only — it can keep an edge main already has, never remove one — which is the right direction. This project has a standing rule that suppressors are scoped per language by evidence; your "per topology, not per language" argument is a serious one and I will take it to that decision rather than ask you to guess the answer. Nothing to do on your side until I come back on it.

Landing order, so you do not resolve a conflict twice

A change of ours, #2227, edits the same drop_plain_call expression in pass_calls.c and pass_parallel.c and will conflict textually with this PR there (registry.c, pipeline.h and both test files auto-merge). The resolution is mechanical and the semantics compose — #2227's exemption covers member calls, yours fires only on bare callees:

(weak_member && !cbm_weak_member_unique_name_exempt(...)) || weak_local_binding ||
    cbm_suppress_external_import_shadow(...)

Since this needs one more push anyway, the cheapest order for you is: we land #2227, you merge main in, and resolve that one expression in the same push as the incremental fix. I will tell you here the moment #2227 is in. (main also currently carries two reds that are not yours — a linter ratchet and a worker-policy script, both fixed by #2257 — so expect your CI to show them until that lands.)

Not checked on my side, so you know the limits of this review: no corpus re-run (the census numbers are yours and mine from earlier in the thread; the incremental finding is proven on the fixture, not measured on a real repository), macOS arm64 only, and the new Windows drive-letter/UNC specifier handling was read, not executed.

This is close. Thank you for the persistence — a month on one PR is a lot to ask of anyone.

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

Labels

bug Something isn't working maintainer-notes Internal observations and improvement notes parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants