devlensio 0.6.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +89 -31
  2. package/dist/extractors/detectLanguage.d.ts +2 -0
  3. package/dist/extractors/detectLanguage.js +33 -0
  4. package/dist/extractors/index.d.ts +6 -0
  5. package/dist/extractors/index.js +155 -0
  6. package/dist/extractors/runner.d.ts +7 -0
  7. package/dist/extractors/runner.js +194 -0
  8. package/dist/extractors/types.d.ts +37 -0
  9. package/dist/extractors/types.js +8 -0
  10. package/dist/graph/buildLookup.d.ts +1 -0
  11. package/dist/graph/buildLookup.js +2 -1
  12. package/dist/graph/edges/callEdges.js +19 -5
  13. package/dist/graph/edges/callEdges.test.d.ts +1 -0
  14. package/dist/graph/edges/callEdges.test.js +200 -0
  15. package/dist/graph/edges/importEdges.js +12 -0
  16. package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
  17. package/dist/graph/edges/inheritanceEdges.js +73 -0
  18. package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
  19. package/dist/graph/edges/inheritanceEdges.test.js +140 -0
  20. package/dist/graph/index.js +4 -0
  21. package/dist/parser/classes.test.d.ts +1 -0
  22. package/dist/parser/classes.test.js +360 -0
  23. package/dist/parser/extractors/classes.d.ts +5 -0
  24. package/dist/parser/extractors/classes.js +241 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +7 -1
  27. package/dist/pipeline/index.d.ts +2 -1
  28. package/dist/pipeline/index.js +30 -42
  29. package/dist/scoring/index.js +8 -0
  30. package/dist/scoring/index.test.js +43 -0
  31. package/dist/scoring/nodeScorer.js +2 -0
  32. package/dist/scoring/pruneDisconnected.d.ts +8 -0
  33. package/dist/scoring/pruneDisconnected.js +66 -0
  34. package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
  35. package/dist/scoring/pruneDisconnected.test.js +123 -0
  36. package/dist/summarizer/prompts.d.ts +1 -1
  37. package/dist/types.d.ts +5 -5
  38. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  39. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  40. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  41. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  42. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  43. package/extractors/go/build.mjs +43 -0
  44. package/extractors/go/calls.go +289 -0
  45. package/extractors/go/contract.go +140 -0
  46. package/extractors/go/extractor.go +161 -0
  47. package/extractors/go/fingerprint.go +167 -0
  48. package/extractors/go/go.mod +3 -0
  49. package/extractors/go/imports.go +90 -0
  50. package/extractors/go/inheritance.go +138 -0
  51. package/extractors/go/lookup.go +106 -0
  52. package/extractors/go/main.go +57 -0
  53. package/extractors/go/nodes.go +177 -0
  54. package/extractors/go/orm_edges.go +277 -0
  55. package/extractors/go/parser.go +469 -0
  56. package/extractors/go/routes.go +521 -0
  57. package/extractors/go/tests.go +45 -0
  58. package/extractors/go/thirdparty.go +138 -0
  59. package/extractors/go/typeload.go +178 -0
  60. package/extractors/go/walker.go +67 -0
  61. package/extractors/java/build.mjs +90 -0
  62. package/extractors/java/devlens_java_extractor.jar +0 -0
  63. package/extractors/java/src/devlens/extractor/Contract.java +171 -0
  64. package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
  65. package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
  66. package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
  67. package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
  68. package/extractors/java/src/devlens/extractor/Main.java +66 -0
  69. package/extractors/java/src/devlens/extractor/Parser.java +522 -0
  70. package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
  71. package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
  72. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
  73. package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
  74. package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
  75. package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
  76. package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
  77. package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
  78. package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
  79. package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
  80. package/extractors/python/devlens_extractors_python/__init__.py +3 -0
  81. package/extractors/python/devlens_extractors_python/__main__.py +33 -0
  82. package/extractors/python/devlens_extractors_python/contract.py +94 -0
  83. package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
  84. package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
  85. package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
  86. package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
  87. package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
  88. package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
  89. package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
  90. package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
  91. package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
  92. package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
  93. package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
  94. package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
  95. package/extractors/python/devlens_extractors_python/extractor.py +107 -0
  96. package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
  97. package/extractors/python/devlens_extractors_python/lookup.py +72 -0
  98. package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
  99. package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
  100. package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
  101. package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
  102. package/extractors/python/devlens_extractors_python/third_party.py +103 -0
  103. package/extractors/python/pyproject.toml +16 -0
  104. package/extractors/python/setup.mjs +54 -0
  105. package/extractors/rust/Cargo.toml +29 -0
  106. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  107. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  108. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  109. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  110. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  111. package/extractors/rust/build.mjs +85 -0
  112. package/extractors/rust/src/calls.rs +295 -0
  113. package/extractors/rust/src/contract.rs +236 -0
  114. package/extractors/rust/src/enrich.rs +107 -0
  115. package/extractors/rust/src/extractor.rs +199 -0
  116. package/extractors/rust/src/fingerprint.rs +238 -0
  117. package/extractors/rust/src/imports.rs +101 -0
  118. package/extractors/rust/src/inheritance.rs +233 -0
  119. package/extractors/rust/src/lookup.rs +226 -0
  120. package/extractors/rust/src/main.rs +53 -0
  121. package/extractors/rust/src/module_map.rs +131 -0
  122. package/extractors/rust/src/nodes.rs +174 -0
  123. package/extractors/rust/src/orm_edges.rs +125 -0
  124. package/extractors/rust/src/parser.rs +859 -0
  125. package/extractors/rust/src/routes.rs +1265 -0
  126. package/extractors/rust/src/tests.rs +109 -0
  127. package/extractors/rust/src/thirdparty.rs +114 -0
  128. package/extractors/rust/src/walker.rs +82 -0
  129. package/package.json +22 -4
@@ -0,0 +1,167 @@
1
+ // fingerprint.go — go.mod parsing → framework / projectType / databases /
2
+ // rawDependencies. Hand-rolled (bufio.Scanner): the binary must stay stdlib-
3
+ // only, so golang.org/x/mod/modfile is out. Never executes the manifest
4
+ // (same security rule as setup.py / pom.xml).
5
+
6
+ package main
7
+
8
+ import (
9
+ "bufio"
10
+ "os"
11
+ "path/filepath"
12
+ "strings"
13
+ )
14
+
15
+ // modFile holds everything the extractor needs from go.mod.
16
+ type modFile struct {
17
+ ModulePath string
18
+ GoVersion string
19
+ Requires map[string]string // module path → version
20
+ Replaces map[string]string // old module path → new module path (replace directives)
21
+ HasMod bool
22
+ }
23
+
24
+ // parseGoMod reads <repo>/go.mod. Missing file → empty modFile (HasMod=false),
25
+ // which the rest of the extractor treats as "not a Go module" gracefully.
26
+ func parseGoMod(repoPath string) *modFile {
27
+ mf := &modFile{Requires: map[string]string{}, Replaces: map[string]string{}}
28
+ f, err := os.Open(filepath.Join(repoPath, "go.mod"))
29
+ if err != nil {
30
+ return mf
31
+ }
32
+ defer f.Close()
33
+
34
+ sc := bufio.NewScanner(f)
35
+ sc.Buffer(make([]byte, 1024*1024), 1024*1024)
36
+ inRequire := false
37
+ for sc.Scan() {
38
+ line := strings.TrimSpace(stripGoModComment(sc.Text()))
39
+ if line == "" {
40
+ continue
41
+ }
42
+ if strings.HasPrefix(line, "require (") {
43
+ inRequire = true
44
+ continue
45
+ }
46
+ if inRequire {
47
+ if line == ")" {
48
+ inRequire = false
49
+ continue
50
+ }
51
+ if mod, ver, ok := splitModuleVersion(line); ok {
52
+ mf.Requires[mod] = ver
53
+ }
54
+ continue
55
+ }
56
+ switch {
57
+ case strings.HasPrefix(line, "module "):
58
+ mf.ModulePath = strings.TrimSpace(strings.TrimPrefix(line, "module "))
59
+ mf.HasMod = true
60
+ case strings.HasPrefix(line, "go "):
61
+ mf.GoVersion = strings.TrimSpace(strings.TrimPrefix(line, "go "))
62
+ case strings.HasPrefix(line, "require "):
63
+ if mod, ver, ok := splitModuleVersion(strings.TrimPrefix(line, "require ")); ok {
64
+ mf.Requires[mod] = ver
65
+ }
66
+ case strings.HasPrefix(line, "replace "):
67
+ // replace old [ver] => new [ver]
68
+ rest := strings.TrimPrefix(line, "replace ")
69
+ if i := strings.Index(rest, "=>"); i >= 0 {
70
+ old := strings.TrimSpace(rest[:i])
71
+ newp := strings.TrimSpace(rest[i+2:])
72
+ // strip versions from the old side: "old v1.2.3" → "old"
73
+ old = strings.Fields(old)[0]
74
+ // new side may be "mod v1.2.3" or a filesystem path
75
+ fields := strings.Fields(newp)
76
+ if len(fields) > 0 {
77
+ mf.Replaces[old] = fields[0]
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return mf
83
+ }
84
+
85
+ // stripGoModComment removes // comments (not inside strings — go.mod has no
86
+ // string literals in require/module lines, so a plain cut is safe here).
87
+ func stripGoModComment(line string) string {
88
+ if i := strings.Index(line, "//"); i >= 0 {
89
+ return line[:i]
90
+ }
91
+ return line
92
+ }
93
+
94
+ // splitModuleVersion splits "mod/path v1.2.3" → (mod/path, v1.2.3).
95
+ func splitModuleVersion(s string) (string, string, bool) {
96
+ fields := strings.Fields(s)
97
+ if len(fields) < 2 {
98
+ return "", "", false
99
+ }
100
+ return fields[0], fields[1], true
101
+ }
102
+
103
+ // ── framework / database detection (documented dep-name conventions) ──
104
+
105
+ var frameworkDeps = []struct {
106
+ dep string
107
+ fw string
108
+ }{
109
+ {"github.com/gin-gonic/gin", "gin"},
110
+ {"github.com/labstack/echo", "echo"},
111
+ {"github.com/gofiber/fiber", "fiber"},
112
+ }
113
+
114
+ var databaseDeps = []struct {
115
+ prefix string
116
+ db string
117
+ }{
118
+ {"gorm.io/gorm", "gorm"},
119
+ {"github.com/jackc/pgx", "postgresql"},
120
+ {"github.com/lib/pq", "postgresql"},
121
+ {"github.com/go-sql-driver/mysql", "mysql"},
122
+ {"modernc.org/sqlite", "sqlite"},
123
+ {"github.com/mattn/go-sqlite3", "sqlite"},
124
+ {"go.mongodb.org/mongo-driver", "mongo"},
125
+ }
126
+
127
+ // fingerprintFromMod — manifest-only detection. web-framework / database list
128
+ // derived from the require set; net-http and database/sql (both stdlib) are
129
+ // upgraded later by extractor.go once imports are known.
130
+ func fingerprintFromMod(mf *modFile) *Fingerprint {
131
+ fp := newFingerprint()
132
+ if !mf.HasMod {
133
+ return fp
134
+ }
135
+ fp.RawDependencies = sortedCopy(mf.Requires)
136
+ for _, fw := range frameworkDeps {
137
+ if hasPrefixKey(mf.Requires, fw.dep) {
138
+ fp.Framework = fw.fw
139
+ fp.ProjectType = "backend"
140
+ break
141
+ }
142
+ }
143
+ seen := map[string]bool{}
144
+ for _, dd := range databaseDeps {
145
+ if hasPrefixKey(mf.Requires, dd.prefix) && !seen[dd.db] {
146
+ fp.Databases = append(fp.Databases, dd.db)
147
+ seen[dd.db] = true
148
+ }
149
+ }
150
+ return fp
151
+ }
152
+
153
+ // hasPrefixKey — require keys can be "gorm.io/gorm" or "github.com/gofiber/fiber/v2".
154
+ func hasPrefixKey(m map[string]string, prefix string) bool {
155
+ for k := range m {
156
+ if k == prefix || strings.HasPrefix(k, prefix+"/") {
157
+ return true
158
+ }
159
+ }
160
+ return false
161
+ }
162
+
163
+ func sortedCopy(m map[string]string) map[string]string {
164
+ // map ordering doesn't matter for JSON (Go marshals maps sorted), but keep
165
+ // the helper honest.
166
+ return m
167
+ }
@@ -0,0 +1,3 @@
1
+ module devlensgoextractor
2
+
3
+ go 1.26
@@ -0,0 +1,90 @@
1
+ // imports.go — IMPORTS edges + [mod] third-party nodes + the local-package
2
+ // membership index. No PACKAGE nodes (removed 2026-08-17 — engine-wide
3
+ // connected-only pruning makes package stubs obsolete; import edges now
4
+ // target the imported package's FILES, matching JS/Java/Rust).
5
+ //
6
+ // Resolution (documented Go semantics): an import path is internal when it
7
+ // starts with the module path (honoring `replace` directives, including
8
+ // filesystem replaces) — otherwise external → [mod]/... node (gated). std
9
+ // packages (no dot in first path element) → no node, no edge. Go imports
10
+ // PACKAGES, so the IMPORTS edge fans out from the importing FILE to every
11
+ // non-test FILE of the target package (one edge per file; package identity
12
+ // rides in each file's metadata.package).
13
+
14
+ package main
15
+
16
+ import (
17
+ "path/filepath"
18
+ "strings"
19
+ )
20
+
21
+ // detectImports — returns IMPORTS edges + seeds the shared ThirdPartyRegistry
22
+ // (imports/calls/inheritance share one gated registry per extraction) and the
23
+ // PkgNodesByPath membership index (local-vs-external checks in
24
+ // calls/routes/orm/inheritance — key presence only, values unused).
25
+ func detectImports(pr *parsedRepo, l *LookupMaps, opts *Options, fp *Fingerprint) []map[string]any {
26
+ edges := []map[string]any{}
27
+
28
+ l.tp = newThirdPartyRegistry(opts.IncludeThirdPartyLibs, fp, l)
29
+
30
+ // Index: import path → non-test files of that package (FILE→FILE targets),
31
+ // plus import path → dir for filesystem-replace resolution.
32
+ filesByImportPath := map[string][]string{}
33
+ dirByImportPath := map[string]string{}
34
+ for _, pkg := range pr.packages {
35
+ l.PkgNodesByPath[pkg.ImportPath] = nil // membership marker only
36
+ relFiles := []string{}
37
+ for _, pf := range pkg.Files {
38
+ if pf.IsTest {
39
+ continue // test files are never import targets (leaf rule)
40
+ }
41
+ relFiles = append(relFiles, pf.RelPath)
42
+ }
43
+ filesByImportPath[pkg.ImportPath] = relFiles
44
+ dirByImportPath[pkg.ImportPath] = pkg.Dir
45
+ }
46
+ // replace-directive aliases: import path → local dir (filesystem replaces)
47
+ fsReplaceDir := map[string]string{}
48
+ for old, newp := range pr.mod.Replaces {
49
+ if strings.HasPrefix(newp, "./") || strings.HasPrefix(newp, "../") || strings.HasPrefix(newp, "/") {
50
+ fsReplaceDir[old] = strings.TrimPrefix(filepath.ToSlash(newp), "./")
51
+ }
52
+ }
53
+
54
+ for _, pf := range pr.files {
55
+ fileID := "file::" + pf.RelPath
56
+ for _, imp := range pf.Imports {
57
+ p := imp.Path
58
+ if isStdImport(p) {
59
+ continue // std: no node, no edge
60
+ }
61
+ // internal? (a) module-prefixed → package files, (b) replace →
62
+ // filesystem dir → that package's files
63
+ targets, ok := filesByImportPath[p]
64
+ if !ok {
65
+ if dir, ok := fsReplaceDir[p]; ok {
66
+ for ip, d := range dirByImportPath {
67
+ if strings.TrimPrefix(filepath.ToSlash(d), "./") == dir {
68
+ targets = filesByImportPath[ip]
69
+ ok = true
70
+ break
71
+ }
72
+ }
73
+ }
74
+ }
75
+ if ok {
76
+ for _, rel := range targets {
77
+ edges = append(edges, edgeWithMeta(fileID, "file::"+rel, EdgeImports,
78
+ map[string]any{"importPath": p}))
79
+ }
80
+ continue
81
+ }
82
+ // external → [mod]/... (gated)
83
+ if n := l.tp.packageNode(p); n != nil {
84
+ edges = append(edges, edgeWithMeta(fileID, n["id"].(string), EdgeImports,
85
+ map[string]any{"importPath": p, "isThirdParty": true}))
86
+ }
87
+ }
88
+ }
89
+ return edges
90
+ }
@@ -0,0 +1,138 @@
1
+ // inheritance.go — EXTENDS (struct embedding / interface embedding) +
2
+ // IMPLEMENTS (go/types.Implements — the gold standard: exact method-set
3
+ // satisfaction, pointer receivers included, no guessing).
4
+
5
+ package main
6
+
7
+ import (
8
+ "go/types"
9
+ "strings"
10
+ )
11
+
12
+ func detectInheritance(pr *parsedRepo, l *LookupMaps, ti *TypeInfo, opts *Options) []map[string]any {
13
+ edges := []map[string]any{}
14
+
15
+ // ── EXTENDS: embedding ──
16
+ for _, pf := range pr.files {
17
+ for _, st := range pf.Structs {
18
+ from := structNodeID(pf.RelPath, st.Name)
19
+ for _, emb := range st.Embedded {
20
+ target := resolveTypeRef(emb, pf, st.PkgPath, l, opts)
21
+ if target != "" {
22
+ edges = append(edges, edgeWithMeta(from, target, EdgeExtends, map[string]any{"embedded": emb}))
23
+ }
24
+ }
25
+ }
26
+ for _, it := range pf.Interfaces {
27
+ from := interfaceNodeID(pf.RelPath, it.Name)
28
+ for _, emb := range it.Embedded {
29
+ target := resolveTypeRef(emb, pf, it.PkgPath, l, opts)
30
+ if target != "" {
31
+ edges = append(edges, edgeWithMeta(from, target, EdgeExtends, map[string]any{"embedded": emb}))
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ // ── IMPLEMENTS: exact via go/types ──
38
+ if ti != nil {
39
+ for _, pkg := range pr.packages {
40
+ tp, ok := ti.Pkgs[pkg.ImportPath]
41
+ if !ok {
42
+ continue
43
+ }
44
+ scope := tp.Scope()
45
+ // local interfaces of this package
46
+ type ifaceInfo struct {
47
+ id string
48
+ typ *types.Interface
49
+ }
50
+ var ifaces []ifaceInfo
51
+ for _, pf := range pkg.Files {
52
+ for _, it := range pf.Interfaces {
53
+ obj := scope.Lookup(it.Name)
54
+ if obj == nil {
55
+ continue
56
+ }
57
+ tn, ok := obj.(*types.TypeName)
58
+ if !ok {
59
+ continue
60
+ }
61
+ if i, ok := tn.Type().Underlying().(*types.Interface); ok {
62
+ ifaces = append(ifaces, ifaceInfo{id: interfaceNodeID(pf.RelPath, it.Name), typ: i})
63
+ }
64
+ }
65
+ }
66
+ if len(ifaces) == 0 {
67
+ continue
68
+ }
69
+ // every named type in the package (structs + enums; types without
70
+ // nodes can't get edges — documented)
71
+ for _, name := range scope.Names() {
72
+ obj := scope.Lookup(name)
73
+ tn, ok := obj.(*types.TypeName)
74
+ if !ok {
75
+ continue
76
+ }
77
+ if _, isIface := tn.Type().Underlying().(*types.Interface); isIface {
78
+ continue
79
+ }
80
+ id := typeNodeID(l, pkg.ImportPath, name)
81
+ if id == "" {
82
+ continue
83
+ }
84
+ for _, ifc := range ifaces {
85
+ if types.Implements(tn.Type(), ifc.typ) || types.Implements(types.NewPointer(tn.Type()), ifc.typ) {
86
+ edges = append(edges, edgeWithMeta(id, ifc.id, EdgeImplements, map[string]any{}))
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return edges
93
+ }
94
+
95
+ // resolveTypeRef — embedded type text → node id (local struct/interface node,
96
+ // or gated [mod]/pkg::Member for external embeds like gorm.Model).
97
+ func resolveTypeRef(emb string, pf *ParsedFile, pkgPath string, l *LookupMaps, opts *Options) string {
98
+ name := strings.TrimPrefix(emb, "*")
99
+ // qualified: alias.Type
100
+ if i := strings.Index(name, "."); i >= 0 {
101
+ alias := name[:i]
102
+ member := name[i+1:]
103
+ if impPath, ok := l.SymbolMaps[pf.RelPath][alias]; ok {
104
+ if _, local := l.PkgNodesByPath[impPath]; local {
105
+ if id, ok := l.StructNodeByPkgName[impPath+"::"+member]; ok {
106
+ return id
107
+ }
108
+ if id, ok := l.InterfaceNodeByPkgName[impPath+"::"+member]; ok {
109
+ return id
110
+ }
111
+ return ""
112
+ }
113
+ // external (e.g. gorm.Model) → gated [mod] member node
114
+ if n := l.tp.memberNode(impPath, member); n != nil {
115
+ return n["id"].(string)
116
+ }
117
+ return ""
118
+ }
119
+ return ""
120
+ }
121
+ // unqualified: same package
122
+ if id, ok := l.StructNodeByPkgName[pkgPath+"::"+name]; ok {
123
+ return id
124
+ }
125
+ if id, ok := l.InterfaceNodeByPkgName[pkgPath+"::"+name]; ok {
126
+ return id
127
+ }
128
+ return ""
129
+ }
130
+
131
+ // typeNodeID — node id for a named type (struct), or "" if the type has no
132
+ // node (plain aliases, iota enums — documented; enums dropped 2026-08-17).
133
+ func typeNodeID(l *LookupMaps, pkgPath, name string) string {
134
+ if id, ok := l.StructNodeByPkgName[pkgPath+"::"+name]; ok {
135
+ return id
136
+ }
137
+ return ""
138
+ }
@@ -0,0 +1,106 @@
1
+ // lookup.go — LookupMaps: ONE shared index built once from parsed facts,
2
+ // consumed by every edge detector. Edge resolution is dict lookups only —
3
+ // never re-walks ASTs (playbook rule).
4
+
5
+ package main
6
+
7
+ import "strings"
8
+
9
+ type LookupMaps struct {
10
+ // node id → node map (all emitted nodes register here)
11
+ NodeByID map[string]map[string]any
12
+ // file node per rel path
13
+ FileNodesByPath map[string]map[string]any
14
+ // PACKAGE node per import path
15
+ PkgNodesByPath map[string]map[string]any
16
+ // symbol maps: rel file → import alias → import path (the bridge for
17
+ // Calls/Routes/Tests — mirrors python's symbol_maps)
18
+ SymbolMaps map[string]map[string]string
19
+ // pkgPath::Name → node id (exact per-package resolution; Go type names
20
+ // are unique per package, so no closest_by_path needed)
21
+ FuncNodeByPkgName map[string]string
22
+ MethodNodeByPkgTypeName map[string]string
23
+ StructNodeByPkgName map[string]string
24
+ InterfaceNodeByPkgName map[string]string
25
+ // third-party registry output (gated) — [mod]/... nodes
26
+ thirdPartyNodes []map[string]any
27
+ thirdPartyByID map[string]map[string]any
28
+ // gated [mod] node factory (imports/calls/inheritance share ONE registry)
29
+ tp *ThirdPartyRegistry
30
+ }
31
+
32
+ func buildLookupMaps(pr *parsedRepo, ti *TypeInfo) *LookupMaps {
33
+ l := &LookupMaps{
34
+ NodeByID: map[string]map[string]any{},
35
+ FileNodesByPath: map[string]map[string]any{},
36
+ PkgNodesByPath: map[string]map[string]any{},
37
+ SymbolMaps: map[string]map[string]string{},
38
+ FuncNodeByPkgName: map[string]string{},
39
+ MethodNodeByPkgTypeName: map[string]string{},
40
+ StructNodeByPkgName: map[string]string{},
41
+ InterfaceNodeByPkgName: map[string]string{},
42
+ thirdPartyByID: map[string]map[string]any{},
43
+ }
44
+ pkgNameByPath := map[string]string{}
45
+ for _, pkg := range pr.packages {
46
+ pkgNameByPath[pkg.ImportPath] = pkg.Name
47
+ }
48
+ for _, pkg := range pr.packages {
49
+ for _, pf := range pkg.Files {
50
+ // symbol map for this file
51
+ syms := map[string]string{}
52
+ for _, imp := range pf.Imports {
53
+ if imp.Alias == "_" || imp.Alias == "." {
54
+ continue
55
+ }
56
+ alias := imp.Alias
57
+ if alias == "" {
58
+ if name, ok := pkgNameByPath[imp.Path]; ok {
59
+ alias = name
60
+ } else {
61
+ alias = lastPathElem(imp.Path)
62
+ }
63
+ }
64
+ syms[alias] = imp.Path
65
+ }
66
+ l.SymbolMaps[pf.RelPath] = syms
67
+
68
+ // code-node id maps
69
+ for _, fn := range pf.Funcs {
70
+ if fn.IsMethod {
71
+ l.MethodNodeByPkgTypeName[fn.PkgPath+"::"+fn.RecvType+"."+fn.Name] = methodNodeID(pf.RelPath, fn)
72
+ } else {
73
+ l.FuncNodeByPkgName[fn.PkgPath+"::"+fn.Name] = funcNodeID(pf.RelPath, fn.Name)
74
+ }
75
+ }
76
+ for _, st := range pf.Structs {
77
+ if st.IsEnum {
78
+ continue // iota enums dropped (2026-08-17) — no node, no edge
79
+ }
80
+ id := structNodeID(pf.RelPath, st.Name)
81
+ l.StructNodeByPkgName[st.PkgPath+"::"+st.Name] = id
82
+ }
83
+ for _, it := range pf.Interfaces {
84
+ l.InterfaceNodeByPkgName[it.PkgPath+"::"+it.Name] = interfaceNodeID(pf.RelPath, it.Name)
85
+ }
86
+ }
87
+ }
88
+ return l
89
+ }
90
+
91
+ // ── node id schemes (deterministic, file-scoped) ──
92
+
93
+ func funcNodeID(rel, name string) string { return rel + "::" + name }
94
+ func methodNodeID(rel string, fn *ParsedFunc) string {
95
+ return rel + "::" + fn.RecvType + "." + fn.Name
96
+ }
97
+ func structNodeID(rel, name string) string { return rel + "::" + name }
98
+ func interfaceNodeID(rel, name string) string { return rel + "::" + name }
99
+ func thirdPartyID(importPath string) string { return "[mod]/" + importPath }
100
+
101
+ func lastPathElem(p string) string {
102
+ if i := strings.LastIndex(p, "/"); i >= 0 {
103
+ return p[i+1:]
104
+ }
105
+ return p
106
+ }
@@ -0,0 +1,57 @@
1
+ // main.go — entry point: stdin JSON {repoPath, options} → stdout JSON
2
+ // ExtractorResult. JSON only on stdout; logs go to stderr.
3
+
4
+ package main
5
+
6
+ import (
7
+ "encoding/json"
8
+ "fmt"
9
+ "io"
10
+ "os"
11
+ )
12
+
13
+ // Input mirrors ExtractorInput from src/extractors/types.ts.
14
+ type Input struct {
15
+ RepoPath string `json:"repoPath"`
16
+ Options json.RawMessage `json:"options"`
17
+ }
18
+
19
+ // Options — the engine forwards includeThirdPartyLibs (both spellings are
20
+ // accepted: engine truth + contract.html docs alias).
21
+ type Options struct {
22
+ IncludeThirdPartyLibs []string `json:"includeThirdPartyLibs"`
23
+ IncludedThirdPartyLibs []string `json:"includedThirdPartyLibs"`
24
+ }
25
+
26
+ func main() {
27
+ raw, err := io.ReadAll(os.Stdin)
28
+ if err != nil {
29
+ fmt.Fprintln(os.Stderr, "error reading stdin:", err)
30
+ os.Exit(1)
31
+ }
32
+ var in Input
33
+ if err := json.Unmarshal(raw, &in); err != nil {
34
+ fmt.Fprintln(os.Stderr, "bad input json:", err)
35
+ os.Exit(1)
36
+ }
37
+ if in.RepoPath == "" {
38
+ fmt.Fprintln(os.Stderr, "repoPath is required")
39
+ os.Exit(1)
40
+ }
41
+ var opts Options
42
+ if len(in.Options) > 0 {
43
+ _ = json.Unmarshal(in.Options, &opts) // malformed options → defaults, non-fatal
44
+ }
45
+ if len(opts.IncludeThirdPartyLibs) == 0 {
46
+ opts.IncludeThirdPartyLibs = opts.IncludedThirdPartyLibs
47
+ }
48
+
49
+ result := runExtractor(in.RepoPath, &opts)
50
+
51
+ out, err := json.Marshal(result)
52
+ if err != nil {
53
+ fmt.Fprintln(os.Stderr, "error marshaling result:", err)
54
+ os.Exit(1)
55
+ }
56
+ os.Stdout.Write(out)
57
+ }