Skip to content

Commit 602bedc

Browse files
aksOpsclaude
andcommitted
fix(detector/typescript): emit anchor nodes for imports edges
The TypeScript structures detector was emitting imports edges with free-form strings (file path → module name) as endpoints, but no matching CodeNode existed for either side. Every imports edge got silently dropped at GraphBuilder.Snapshot's phantom-edge filter. On nuxt (1269 files, mostly TS): 3507 phantom edges out of 6923 total emissions — half of all edges were dropped because the imports detector was sending them into the void. Fix: - Emit a NodeModule for the current file (`ts:file:<path>`) once per file. - Emit a NodeExternal for each imported module (`ts:external:<mod>`) once. - Wire the imports edge through these node IDs. Dedup via the GraphBuilder map collapses the per-file external nodes across files (every file importing "react" gets one shared ts:external:react target), so the graph also gets a real dependency view at no extra cost. Bench (nuxt re-index): - Before: 4902 nodes, 2416 edges, 3507 phantom drops - After: 5914 nodes, 4770 edges, 1153 phantom drops (-67% phantoms) - Deduped: 1807 nodes (external modules collapsed across files) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 83e9493 commit 602bedc

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

go/internal/detector/typescript/structures.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,41 @@ func (d TypeScriptStructuresDetector) Detect(ctx *detector.Context) *detector.Re
143143
nodes = append(nodes, mk(id, model.NodeEnum, name, base.FindLineNumber(text, m[0]), nil))
144144
}
145145

146-
// Imports
146+
// Imports — emit a SOURCE-side file node + an EXTERNAL/MODULE target node
147+
// so the edge survives Snapshot's phantom-drop. Pre-fix, both endpoints
148+
// were free-form strings (the file path and the module name) with no
149+
// matching CodeNode anywhere, so every imports edge got dropped at
150+
// snapshot. On large TypeScript repos (e.g. nuxt) that produced ~50%
151+
// phantom-edge waste in the cache.
152+
fileNodeID := "ts:file:" + fp
153+
importTargets := make(map[string]bool)
147154
for _, m := range tsImportRE.FindAllStringSubmatchIndex(text, -1) {
148155
mod := text[m[2]:m[3]]
149-
e := model.NewCodeEdge(fp+"->imports->"+mod, model.EdgeImports, fp, mod)
156+
if importTargets[mod] {
157+
continue
158+
}
159+
importTargets[mod] = true
160+
// Ensure the file-as-source node exists once.
161+
if !existing[fileNodeID] {
162+
existing[fileNodeID] = true
163+
fn := model.NewCodeNode(fileNodeID, model.NodeModule, fp)
164+
fn.FilePath = fp
165+
fn.Source = "TypeScriptStructuresDetector"
166+
fn.Confidence = model.ConfidenceLexical
167+
fn.Properties["module_type"] = "ts_file"
168+
nodes = append(nodes, fn)
169+
}
170+
// Ensure the external module target node exists.
171+
targetID := "ts:external:" + mod
172+
if !existing[targetID] {
173+
existing[targetID] = true
174+
tn := model.NewCodeNode(targetID, model.NodeExternal, mod)
175+
tn.Source = "TypeScriptStructuresDetector"
176+
tn.Confidence = model.ConfidenceLexical
177+
tn.Properties["module"] = mod
178+
nodes = append(nodes, tn)
179+
}
180+
e := model.NewCodeEdge(fileNodeID+"->imports->"+targetID, model.EdgeImports, fileNodeID, targetID)
150181
e.Source = "TypeScriptStructuresDetector"
151182
e.Confidence = model.ConfidenceLexical
152183
edges = append(edges, e)

go/internal/detector/typescript/structures_test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,25 @@ func TestTypeScriptStructuresPositive(t *testing.T) {
7474
if enums != 1 {
7575
t.Errorf("expected 1 enum, got %d", enums)
7676
}
77-
if modules != 1 {
78-
t.Errorf("expected 1 module (namespace), got %d", modules)
77+
// 1 namespace + 1 file-as-module (anchor for imports edges so they
78+
// survive the GraphBuilder phantom-drop).
79+
if modules != 2 {
80+
t.Errorf("expected 2 modules (namespace + file), got %d", modules)
7981
}
8082
if len(r.Edges) != 2 {
8183
t.Errorf("expected 2 imports, got %d", len(r.Edges))
8284
}
85+
// Each import target should also exist as an EXTERNAL node so the
86+
// edge isn't dropped at snapshot.
87+
var externals int
88+
for _, n := range r.Nodes {
89+
if n.Kind == model.NodeExternal {
90+
externals++
91+
}
92+
}
93+
if externals != 2 {
94+
t.Errorf("expected 2 external module nodes (imports targets), got %d", externals)
95+
}
8396
}
8497

8598
func TestTypeScriptStructuresDeterminism(t *testing.T) {

0 commit comments

Comments
 (0)