fix(extract): stop cross-project edges from platform imports and shared npm deps (#3237) - #3262
Conversation
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. 1 change(s) tested, no difference found (not proven).
Graphify review — findings
Routes JVM/Android platform and Kotlin-stdlib imports (roots like java., android., kotlin.) to the external ref namespace via _JVM_PLATFORM_PACKAGE_ROOTS in _import_java and _import_kotlin, so their bare last-segment targets can no longer byte-collide with unrelated repo nodes or ride the alias index onto same-stem files in a monorepo; unresolvable platform imports drop as external instead of dangling. Mints package.json dependency entries as a shared type="module" node under _make_id("ref", key) in extract_json, so two manifests naming the same npm package collapse to one anchor and JS bare specifiers land on it rather than being joined across projects. Exempts equal-labelled module-vs-module id collisions from the collision warning in _report_id_collision since those shared anchors collapse losslessly by design.
Worth a look
- package.json dependency node ids changed from bare package ids to ref-prefixed ids —
graphify/extractors/json_config.py:200· 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 — 2000 functions depend on the 304 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 524 callers, 43 callees - new:
_rebuild_code()— 113 callers, 50 callees - new:
deduplicate_entities()— 63 callers, 21 callees - new:
build_merge()— 62 callers, 13 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 123 callees - new:
build()— 42 callers, 4 callees - …and 31 more — each is listed as a finding
Verification — 2000 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: 1787 function(s) in the blast radius were not formally verified this run
Formal verification
No difference found (not proven): No behavior difference found in \_report\_id\_collision (not a proof).
The verifier ran both versions of \_report\_id\_collision 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 \_import\_java.
The verifier did not have enough to check \_import\_java, 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 \_import\_kotlin.
The verifier did not have enough to check \_import\_kotlin, 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 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
· 39 more finding(s) on lines outside this diff (see the check run).
There was a problem hiding this comment.
Pull request overview
This PR addresses monorepo cross-project edge pollution caused by name-colliding import targets and shared npm dependency nodes, by introducing a non-collidable external ref namespace for platform imports and registry package anchors.
Changes:
- Namespace JVM/Android/Kotlin platform import targets under
refto prevent collisions with repo-local nodes during build-time alias remapping. - Namespace
package.jsondependency targets underrefand mark them astype="module"to model registry packages as shared external anchors. - Add an end-to-end regression test suite covering the reported collision scenarios and expected resolution behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/test_monorepo_import_collisions.py | Adds regression tests reproducing and preventing cross-project binding via platform imports and shared npm deps. |
| graphify/extractors/json_config.py | Changes dependency target node IDs to ref_* and marks them as type="module" to create a shared registry anchor node. |
| graphify/extract.py | Routes JVM/Android/Kotlin platform imports to ref_* targets to prevent repo-collidable bare-stem IDs. |
| graphify/dedup.py | Suppresses collision warnings for intentional type="module" duplicates with the same normalized label. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| add_node(dep_nid, key, line, file_type="concept", | ||
| node_type="module") |
Closes #3237 (classes 1 and 2).
The problem
Two mechanisms produced confident (EXTRACTED) cross-project edges between symbols that merely share a name — 20 of the 25 cross-project edges the reporter audited:
JVM/Android platform imports emit a repo-collidable bare-stem target.
_import_kotlin/_import_javatarget_make_id(last_segment). Forimport java.util.UUIDthat is the byte-exact id of the npm dependency node another project'spackage.jsonmints foruuid— the reporter's five-file UUID case reproduces verbatim. And when nothing owns the bare id, it can still ride build.py's pre-migration alias index (Silent data loss: doc files with the same name in different directories produce colliding node IDs across extraction chunks #1504) onto whichever unrelated same-stem file uniquely claims it —import android.graphics.Colorin project A binds to project B'sui/theme/Color.ktfile node. Both paths reproduce on 0.9.53 from four-file corpora.package.json dependency entries mint a global bare target node per package name (
_make_id(key)in the json extractor), so two projects that independently installtypescriptwere joined through it — including the reporter's pair pinned to different majors.The change
Both sites get the
refexternal namespace — the same cure_resolve_js_import_targetalready applies to unresolvable JS bare specifiers (#1638), and the J-4 conventionextends/$refvalues already use:java.,javax.,jakarta.,kotlin.,kotlinx.,android.,androidx.,dalvik.) target_make_id("ref", fqn)instead of the bare stem. These roots are never repo-local, so the Kotlin: _KOTLIN_CONFIG declares node typeimport_header, bundled grammar emitsimport— every Kotlin imports edge is silently dropped (45% -> 95% dep recall with a 2-line fix) #2526 corpus resolver could not have rewritten them anyway; the ref target matches no repo node and build drops the edge as an external import — the behavior every other external import already has. Repo-shaped imports keep the bare stem, and the Kotlin: _KOTLIN_CONFIG declares node typeimport_header, bundled grammar emitsimport— every Kotlin imports edge is silently dropped (45% -> 95% dep recall with a 2-line fix) #2526 repo-local rewrite is untouched (covered by a control test)._make_id("ref", key)withtype="module", modeling the registry package as one shared external anchor — the issue's own suggestion.type="module"puts it under the Swiftimportsedges are silently dropped from the final graph #1327 module-anchor exemption in_disambiguate_colliding_node_ids, so N manifests declaring the same package share ONE node instead of being salted apart per file; a matching exemption in dedup's collision reporter stops the "minted by two different files" warning for identical-label module anchors, whose collapse is the intent, not a loss.A side effect worth having: a TS
import { v4 } from "uuid"already targets_make_id("ref", "uuid")(#2457) — it now lands on the manifest's dependency node instead of dangling, so code links to the manifest that declares the package.Class 3 (minified-bundle symbol nodes) is a separate detection question and is not touched here; the non-imported
Result-supertype case in class 1 is the type-stub fragmentation tracked in #3252.Tests
tests/test_monorepo_import_collisions.py— 7 tests: the UUID byte-collision and the Color alias-ride (built end-to-end from the issue's shapes) no longer cross projects; a repo-local Kotlin FQN import still resolves (#2526 control); Kotlin and Java platform imports emitref_-namespaced targets while repo-shaped imports keep the bare stem; two manifests share exactly onetype=moduleregistry node with no direct project-to-project edge; and the TS bare-specifier→manifest bind exists. With the fix reverted, 6 of 7 fail (the #2526 control rightly keeps passing). Related suites unchanged: dedup/kotlin/java/import suites (234 passed) and the json/config/manifest slice (332 passed); the full suite matches a fresh same-versionv8baseline.