Repository navigation
Description
Summary
extract() correctly leaves an unresolved Dart type reference as a dangling stub (punchtype), but build_from_json() then resolves that endpoint through _alias_index() to an unrelated Java file node, because the alias is committed when exactly one node claims the old-stem spelling — with no language check. On a real repo this produces the only cross-language code edge, and it is an articulation edge that dominates betweenness.
Environment
- graphifyy 0.9.100 (pip), Python 3.10, Windows 11
- fresh AST cache (0/3 files served), no LLM involved
Minimal reproduction
Three files:
// mobile/lib/domain/punch.dart
enum PunchType { checkIn, checkOut } // L3
class PunchDraft {
final String employeeId;
final PunchType type; // L27 - the reference
}// backend/.../PunchType.java
public enum PunchType { CHECK_IN, CHECK_OUT }// backend/.../AttendancePunch.java (present so the enum has a same-corpus user)
public record AttendancePunch(UUID deviceId, PunchType type) {}from pathlib import Path
from graphify.extract import extract
from graphify.build import build_from_json
files = [
Path('mobile/lib/domain/punch.dart'),
Path('backend/.../PunchType.java'),
Path('backend/.../AttendancePunch.java'),
]
r = extract(files, cache_root=<fresh dir>, root=Path('.'))
# raw edge: mobile_lib_domain_punch --references--> punchtype (dangling, correct)
G = build_from_json(r, root='.', directed=False)Actual (built)
mobile_lib_domain_punch --references [EXTRACTED, context=variable_type, L27]-->
backend_src_main_java_ec_smartlocalizador_attendance_punchtype
where the target node is labeled PunchType.java with source_file=backend/.../PunchType.java.
Expected
mobile_lib_domain_punch --references--> mobile_lib_domain_punch_punchtype
(label PunchType, defined in the same Dart file at L3), or a dangling stub if same-language cross-file resolution is unavailable. It should never bind to PunchType.java.
Where it happens
extract()emits targetpunchtype(dangling stub) — correct.build.py::_alias_index(around L1142–1218, applied at L1793) registers_old_file_stems(rel)for every node with asource_file. Forbackend/.../PunchType.javathose forms areattendance_punchtypeand the barepunchtype. The Dart endpointpunchtypematches the bare alias; the alias has exactly one claimant, so thenorm_to_id.setdefault(...)commit binds it to the Java file node.- The uniqueness rule protects against ambiguity between corpus files, but not against a same-named node in a different language. The comment at L1767–1777 already flags "silent cross-file (and cross-language) merge" as the hazard that uniqueness is meant to prevent — it does not prevent this case, because the correctly-named same-language node is not a claimant of the alias.
Relationship to existing issues
- Cross-language phantom edges: unresolved imports/references bind by name across language boundaries #1749 described this exact family (unresolved imports/references binding by name across language boundaries) and suggested gating both
_rewire_unique_stub_nodesand the build alias index on language compatibility. It is closed as completed, but the alias branch here still has no language gate in 0.9.100. - Interop guard missing on the type-match branch of stub resolution: Swift/Python
Pathbinds to a JS class #2343 covers the type/class branches of_absorb_stubsinextract.py. This report is the separate build-layer alias path, so it is not addressed by that fix either.
Impact (real repo: 1006 nodes / 1513 edges)
- This is the only
dart -> javaedge among all 1513 edges (scanned across dart/java/ts/kotlin). - It is an articulation edge: removing the
PunchTypenode splits the main connected component771 -> 232 + 536, where the 232-node side is the entire Flutter app lobe. - Betweenness inflated:
PunchType0.2495,punch.dart0.2532,AttendancePunch0.2493,PunchType.java0.2468 — all on this one chain. graphify's own GRAPH_REPORT surfaced it as a top God Node and "surprising connection". - Mobile and backend couple over HTTP, never by import; the graph should not connect them here.
Suggested fix
Gate alias resolution on language/extension compatibility at the edge-resolution site (where the edge's source_file is known), reusing the _lang_family helper already used in _rewire_unique_stub_nodes: when looking up an endpoint alias, skip candidates whose source_file language family differs from the edge source's family. Uniqueness is the wrong safety property when the correct answer is "none of the above" — the same conclusion as #1749.
An alternative narrow rule: only honor cross-language aliases for endpoints that are demonstrably stale pre-migration ids (they match a node's _old_file_stems form in the edge's own language), never for bare type names emitted as live references.
I can test a patch against the repro above if useful.
Activity
github-actions commentedon Nov 1, 2026
Thanks for opening this issue, @EthgarHernan. A maintainer will take a look soon.
If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.
To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.
safishamsi commentedon Nov 2, 2026
Fixed in 0.9.101 (published to PyPI) via #4949. The file-stem alias index only rebinds an edge when the edge's file and the alias target share a language family, so a Dart reference no longer binds onto an unrelated same-stemmed Java file.