Skip to content

Commit 8efbc32

Browse files
aksOpsclaude
andcommitted
feat(detector/base): EnsureFileAnchor + EnsureExternalAnchor helpers
Extract the anchor-node pattern used by TypeScript / Python / Rust / C++ imports detectors into shared helpers. Each detector that emits cross-file imports edges now calls: fileID := base.EnsureFileAnchor(ctx, langPrefix, detectorName, conf, &nodes, seen) targetID := base.EnsureExternalAnchor(name, idPrefix, detectorName, conf, &nodes, seen) edges = append(edges, model.NewCodeEdge(fileID+"->imports->"+targetID, ...)) The helpers materialize NodeModule + NodeExternal anchors so imports edges survive GraphBuilder.Snapshot's phantom-edge filter, and the dedup map collapses the per-file and per-external nodes across files for free. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1581b28 commit 8efbc32

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Anchor helpers — Plan §1.2 follow-on.
2+
//
3+
// Many regex detectors emit cross-file "imports" / "depends_on" edges
4+
// using the source file path and the imported name as endpoints. Both
5+
// endpoints were free-form strings with no matching CodeNode, so every
6+
// such edge got dropped at GraphBuilder.Snapshot's phantom filter.
7+
//
8+
// EnsureFileAnchor and EnsureExternalAnchor materialize anchor nodes so
9+
// the edges survive. The GraphBuilder dedup map collapses the per-file
10+
// and per-external nodes across files at zero extra cost (every Python
11+
// file importing "requests" gets one shared py:external:requests node).
12+
package base
13+
14+
import (
15+
"github.com/randomcodespace/codeiq/go/internal/detector"
16+
"github.com/randomcodespace/codeiq/go/internal/model"
17+
)
18+
19+
// EnsureFileAnchor returns the canonical ID of the file-as-module anchor
20+
// node for ctx.FilePath and appends the node to nodes once. Caller must
21+
// pass the same `seen` map across invocations within a single detector
22+
// run (or nil for one-shot calls).
23+
//
24+
// langPrefix scopes the anchor namespace ("py" for Python, "ts" for
25+
// TypeScript, etc.) so cross-language detectors don't collide on the
26+
// same path.
27+
//
28+
// Detector source/confidence are stamped onto the anchor — pick a
29+
// confidence that's at-or-below the actual emission detector so the
30+
// merge rule (higher wins) doesn't accidentally demote a high-confidence
31+
// emission later.
32+
func EnsureFileAnchor(ctx *detector.Context, langPrefix, detectorName string, conf model.Confidence, nodes *[]*model.CodeNode, seen map[string]bool) string {
33+
id := langPrefix + ":file:" + ctx.FilePath
34+
if seen != nil && seen[id] {
35+
return id
36+
}
37+
if seen != nil {
38+
seen[id] = true
39+
}
40+
n := model.NewCodeNode(id, model.NodeModule, ctx.FilePath)
41+
n.FilePath = ctx.FilePath
42+
n.Source = detectorName
43+
n.Confidence = conf
44+
n.Properties["module_type"] = langPrefix + "_file"
45+
*nodes = append(*nodes, n)
46+
return id
47+
}
48+
49+
// EnsureExternalAnchor returns the canonical ID of an external module /
50+
// package / image target and appends it to nodes once per unique name.
51+
// idPrefix scopes the namespace ("py:external", "rust:external",
52+
// "docker:image", etc.).
53+
func EnsureExternalAnchor(name, idPrefix, detectorName string, conf model.Confidence, nodes *[]*model.CodeNode, seen map[string]bool) string {
54+
id := idPrefix + ":" + name
55+
if seen != nil && seen[id] {
56+
return id
57+
}
58+
if seen != nil {
59+
seen[id] = true
60+
}
61+
n := model.NewCodeNode(id, model.NodeExternal, name)
62+
n.Source = detectorName
63+
n.Confidence = conf
64+
n.Properties["module"] = name
65+
*nodes = append(*nodes, n)
66+
return id
67+
}

0 commit comments

Comments
 (0)