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,289 @@
1
+ // calls.go — CALLS resolution ladder.
2
+ //
3
+ // 1. types.Info.Selections[selExpr] → *types.Func → local package → exact
4
+ // METHOD/FUNCTION node; std → skip; external → lazy [mod]/pkg::member
5
+ // (gated), receiver package from the selection's receiver type.
6
+ // 2. types.Info.Uses[ident] → *types.Func → same by name.
7
+ // 3. Name-based fallback (no type info / external receivers): plain name →
8
+ // same-package func; alias.member → symbol maps → local method/func or
9
+ // [mod] lazy member (python's proven chain rule).
10
+ //
11
+ // metadata.calls (sorted unique strings) + metadata.resolvedCalls written on
12
+ // every caller node (contract compliance + LLM context).
13
+
14
+ package main
15
+
16
+ import (
17
+ "go/ast"
18
+ "go/token"
19
+ "go/types"
20
+ "sort"
21
+ "strings"
22
+ )
23
+
24
+ type resolvedCall struct {
25
+ Target string `json:"target"`
26
+ Method string `json:"method,omitempty"`
27
+ Class string `json:"class,omitempty"`
28
+ }
29
+
30
+ // detectCalls — CALLS edges for every non-test function/method body.
31
+ func detectCalls(pr *parsedRepo, ti *TypeInfo, l *LookupMaps, opts *Options) []map[string]any {
32
+ edges := []map[string]any{}
33
+
34
+ for _, pf := range pr.files {
35
+ info := ti.Infos[pf.RelPath]
36
+ for _, fn := range pf.Funcs {
37
+ if fn.IsTest {
38
+ continue // test funcs are leaf metadata, not callers
39
+ }
40
+ callerID := nodeIDForFunc(pf.RelPath, fn)
41
+ callStrings := make([]string, 0, len(fn.Calls))
42
+ var details []resolvedCall
43
+ for _, c := range fn.Calls {
44
+ callStrings = append(callStrings, c.Str)
45
+ target, method, class, e := resolveCall(c, fn, pf, info, l)
46
+ if e != nil {
47
+ edges = append(edges, e)
48
+ }
49
+ if target != "" {
50
+ details = append(details, resolvedCall{Target: target, Method: method, Class: class})
51
+ }
52
+ }
53
+ // metadata enrichment on the (already created) caller node
54
+ if n, ok := l.NodeByID[callerID]; ok {
55
+ m := n["metadata"].(map[string]any)
56
+ m["calls"] = sortedUnique(callStrings)
57
+ if len(details) > 0 {
58
+ m["resolvedCalls"] = details
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return edges
64
+ }
65
+
66
+ // nodeIDForFunc — matches the id scheme in lookup.go.
67
+ func nodeIDForFunc(rel string, fn *ParsedFunc) string {
68
+ if fn.IsMethod {
69
+ return methodNodeID(rel, fn)
70
+ }
71
+ return funcNodeID(rel, fn.Name)
72
+ }
73
+
74
+ // resolveCall → (target node id, method, class, edge) — edge nil when
75
+ // unresolvable (documented: builtins/locals/dynamic stay metadata-only).
76
+ func resolveCall(c CallSite, fn *ParsedFunc, pf *ParsedFile, info *types.Info,
77
+ l *LookupMaps) (string, string, string, map[string]any) {
78
+
79
+ pkgPath := fn.PkgPath
80
+ callerID := nodeIDForFunc(pf.RelPath, fn)
81
+
82
+ // ── 1. type-info resolution (exact) ──
83
+ if info != nil {
84
+ if selExpr, ok := c.Node.(*ast.SelectorExpr); ok {
85
+ if sel, ok := info.Selections[selExpr]; ok {
86
+ if obj, ok := sel.Obj().(*types.Func); ok && obj != nil {
87
+ if id, class, ok := funcToNode(obj, l); ok {
88
+ return id, c.Sel, class, callEdge(callerID, id, c.Sel, class)
89
+ }
90
+ if obj.Pkg() != nil {
91
+ path := obj.Pkg().Path()
92
+ if !isStdImport(path) {
93
+ if n := l.tp.memberNode(path, c.Sel); n != nil {
94
+ return n["id"].(string), c.Sel, "", callEdge(callerID, n["id"].(string), c.Sel, "")
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ } else if idExpr, ok := c.Node.(*ast.Ident); ok {
101
+ if obj, ok := info.Uses[idExpr].(*types.Func); ok && obj != nil {
102
+ if id, class, ok := funcToNode(obj, l); ok {
103
+ return id, "", class, callEdge(callerID, id, "", class)
104
+ }
105
+ if obj.Pkg() != nil {
106
+ path := obj.Pkg().Path()
107
+ if !isStdImport(path) {
108
+ if n := l.tp.memberNode(path, obj.Name()); n != nil {
109
+ return n["id"].(string), obj.Name(), "", callEdge(callerID, n["id"].(string), obj.Name(), "")
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ // ── 1.5 receiver-type-from-declaration (params / typed vars) ──
118
+ // Handlers receive framework types by signature: func(c *gin.Context) —
119
+ // the checker can't resolve external types, but the DECLARED type can.
120
+ if c.IsSel {
121
+ if impPath, typeName, local := receiverDeclaredType(c, fn, pf, l); typeName != "" {
122
+ if local {
123
+ if id, ok := l.MethodNodeByPkgTypeName[impPath+"::"+typeName+"."+c.Sel]; ok {
124
+ return id, c.Sel, typeName, callEdge(callerID, id, c.Sel, typeName)
125
+ }
126
+ } else if n := l.tp.memberNode(impPath, typeName+"."+c.Sel); n != nil {
127
+ return n["id"].(string), c.Sel, typeName, callEdge(callerID, n["id"].(string), c.Sel, typeName)
128
+ }
129
+ }
130
+ }
131
+
132
+ // ── 2. name-based fallback ──
133
+ if c.IsSel {
134
+ // alias.member: receiver is an import alias → local member or [mod]
135
+ alias := firstSegment(c.Receiver)
136
+ if impPath, ok := l.SymbolMaps[pf.RelPath][alias]; ok {
137
+ if _, local := l.PkgNodesByPath[impPath]; local {
138
+ if id, ok := l.MethodNodeByPkgTypeName[impPath+"::"+c.Sel]; ok {
139
+ return id, c.Sel, "", callEdge(callerID, id, c.Sel, "")
140
+ }
141
+ if id, ok := l.FuncNodeByPkgName[impPath+"::"+c.Sel]; ok {
142
+ return id, c.Sel, "", callEdge(callerID, id, c.Sel, "")
143
+ }
144
+ return "", "", "", nil
145
+ }
146
+ if n := l.tp.memberNode(impPath, c.Sel); n != nil {
147
+ return n["id"].(string), c.Sel, "", callEdge(callerID, n["id"].(string), c.Sel, "")
148
+ }
149
+ return "", "", "", nil
150
+ }
151
+ // receiver names a local TYPE (User.Create) → method on that type
152
+ if id, ok := l.MethodNodeByPkgTypeName[pkgPath+"::"+c.Receiver+"."+c.Sel]; ok {
153
+ return id, c.Sel, c.Receiver, callEdge(callerID, id, c.Sel, c.Receiver)
154
+ }
155
+ // receiver is a plain variable whose type we can't see — unresolvable
156
+ // tier (documented; type info covers the exact cases)
157
+ return "", "", "", nil
158
+ }
159
+
160
+ // plain name: same package first
161
+ if id, ok := l.FuncNodeByPkgName[pkgPath+"::"+c.Str]; ok {
162
+ return id, "", "", callEdge(callerID, id, "", "")
163
+ }
164
+ return "", "", "", nil
165
+ }
166
+
167
+ func callEdge(from, to, method, class string) map[string]any {
168
+ meta := map[string]any{}
169
+ if method != "" {
170
+ meta["method"] = method
171
+ }
172
+ if class != "" {
173
+ meta["class"] = class
174
+ }
175
+ return edgeWithMeta(from, to, EdgeCalls, meta)
176
+ }
177
+
178
+ // receiverDeclaredType — the receiver's type from its DECLARATION: function
179
+ // parameters first (func(c *gin.Context) — the documented handler signature),
180
+ // then package-level typed vars (var DB *gorm.DB). Returns (importPath,
181
+ // typeName, local) — local=false means external (memberNode candidate).
182
+ func receiverDeclaredType(c CallSite, fn *ParsedFunc, pf *ParsedFile, l *LookupMaps) (string, string, bool) {
183
+ recvName := firstSegment(c.Receiver)
184
+ if recvName == "" {
185
+ return "", "", false
186
+ }
187
+ // 1. function parameters
188
+ if fn.Decl != nil && fn.Decl.Type != nil && fn.Decl.Type.Params != nil {
189
+ for _, field := range fn.Decl.Type.Params.List {
190
+ for _, name := range field.Names {
191
+ if name.Name == recvName {
192
+ return declaredTypeInfo(field.Type, fn.PkgPath, pf, l)
193
+ }
194
+ }
195
+ }
196
+ }
197
+ // 2. package-level typed vars in this file (var DB *gorm.DB)
198
+ for _, decl := range pf.AST.Decls {
199
+ gd, ok := decl.(*ast.GenDecl)
200
+ if !ok || gd.Tok != token.VAR {
201
+ continue
202
+ }
203
+ for _, spec := range gd.Specs {
204
+ vs, ok := spec.(*ast.ValueSpec)
205
+ if !ok || vs.Type == nil {
206
+ continue
207
+ }
208
+ for _, name := range vs.Names {
209
+ if name.Name == recvName {
210
+ return declaredTypeInfo(vs.Type, fn.PkgPath, pf, l)
211
+ }
212
+ }
213
+ }
214
+ }
215
+ return "", "", false
216
+ }
217
+
218
+ // declaredTypeInfo — a declared type expr → (importPath, typeName, local).
219
+ // Ident "User" → local; SelectorExpr "gin.Context" → alias → external path.
220
+ func declaredTypeInfo(expr ast.Expr, pkgPath string, pf *ParsedFile, l *LookupMaps) (string, string, bool) {
221
+ switch t := expr.(type) {
222
+ case *ast.StarExpr:
223
+ return declaredTypeInfo(t.X, pkgPath, pf, l)
224
+ case *ast.Ident:
225
+ return pkgPath, t.Name, true // same package — local type
226
+ case *ast.SelectorExpr:
227
+ alias := identName(t.X)
228
+ if impPath, ok := l.SymbolMaps[pf.RelPath][alias]; ok {
229
+ return impPath, t.Sel.Name, false
230
+ }
231
+ case *ast.IndexExpr:
232
+ return declaredTypeInfo(t.X, pkgPath, pf, l)
233
+ case *ast.IndexListExpr:
234
+ return declaredTypeInfo(t.X, pkgPath, pf, l)
235
+ }
236
+ return "", "", false
237
+ }
238
+
239
+ // funcToNode — *types.Func declared in a local package → (node id, typeName).
240
+ func funcToNode(obj *types.Func, l *LookupMaps) (string, string, bool) {
241
+ if obj.Pkg() == nil {
242
+ return "", "", false
243
+ }
244
+ pkgPath := obj.Pkg().Path()
245
+ if _, local := l.PkgNodesByPath[pkgPath]; !local {
246
+ return "", "", false
247
+ }
248
+ if recv := obj.Type().(*types.Signature).Recv(); recv != nil {
249
+ typeName := recvTypeNameFromTypes(recv.Type())
250
+ if typeName == "" {
251
+ return "", "", false
252
+ }
253
+ id, ok := l.MethodNodeByPkgTypeName[pkgPath+"::"+typeName+"."+obj.Name()]
254
+ return id, typeName, ok
255
+ }
256
+ id, ok := l.FuncNodeByPkgName[pkgPath+"::"+obj.Name()]
257
+ return id, "", ok
258
+ }
259
+
260
+ // recvTypeNameFromTypes — receiver type → simple name (strips * and pkg path).
261
+ func recvTypeNameFromTypes(t types.Type) string {
262
+ switch tt := t.(type) {
263
+ case *types.Pointer:
264
+ return recvTypeNameFromTypes(tt.Elem())
265
+ case *types.Named:
266
+ return tt.Obj().Name()
267
+ }
268
+ return ""
269
+ }
270
+
271
+ func firstSegment(s string) string {
272
+ if i := strings.Index(s, "."); i >= 0 {
273
+ return s[:i]
274
+ }
275
+ return s
276
+ }
277
+
278
+ func sortedUnique(list []string) []string {
279
+ seen := map[string]bool{}
280
+ out := []string{}
281
+ for _, s := range list {
282
+ if !seen[s] {
283
+ seen[s] = true
284
+ out = append(out, s)
285
+ }
286
+ }
287
+ sort.Strings(out)
288
+ return out
289
+ }
@@ -0,0 +1,140 @@
1
+ // contract.go — the JSON shape DevLens Engine expects.
2
+ //
3
+ // Every key here is camelCase to match the TypeScript interfaces in the
4
+ // devlens-engine (src/types.ts, src/extractors/types.ts). This file is the
5
+ // single source of truth for the output shape; other modules never spell
6
+ // keys by hand.
7
+
8
+ package main
9
+
10
+ import (
11
+ "crypto/sha256"
12
+ "encoding/hex"
13
+ "path"
14
+ )
15
+
16
+ // ── Node types (subset of the engine's NodeType union used by Go) ──
17
+ const (
18
+ NodeFile = "FILE"
19
+ NodeFunction = "FUNCTION"
20
+ NodeMethod = "METHOD"
21
+ NodeStruct = "STRUCT"
22
+ NodeInterface = "INTERFACE"
23
+ NodePackage = "PACKAGE"
24
+ NodeTest = "TEST"
25
+ NodeRoute = "ROUTE"
26
+ NodeThirdParty = "THIRD_PARTY"
27
+ NodeEnum = "ENUM"
28
+ )
29
+
30
+ // ── Edge types (subset of the engine's EdgeType union used by Go) ──
31
+ const (
32
+ EdgeCalls = "CALLS"
33
+ EdgeImports = "IMPORTS"
34
+ EdgeExtends = "EXTENDS"
35
+ EdgeImplements = "IMPLEMENTS"
36
+ EdgeHandles = "HANDLES"
37
+ EdgeTests = "TESTS"
38
+ EdgeReadsFrom = "READS_FROM"
39
+ EdgeWritesTo = "WRITES_TO"
40
+ )
41
+
42
+ // codeHash = sha256(rawCode) hex-encoded, first 16 chars (contract rule).
43
+ func codeHash(raw string) string {
44
+ sum := sha256.Sum256([]byte(raw))
45
+ return hex.EncodeToString(sum[:])[:16]
46
+ }
47
+
48
+ // ── Fingerprint ──
49
+
50
+ type Fingerprint struct {
51
+ Language string `json:"language"`
52
+ ProjectType string `json:"projectType"`
53
+ Framework string `json:"framework"`
54
+ Router string `json:"router"`
55
+ StateManagement []string `json:"stateManagement"`
56
+ DataFetching []string `json:"dataFetching"`
57
+ Databases []string `json:"databases"`
58
+ RawDependencies map[string]string `json:"rawDependencies"`
59
+ }
60
+
61
+ func newFingerprint() *Fingerprint {
62
+ return &Fingerprint{
63
+ Language: "go",
64
+ ProjectType: "unknown",
65
+ Framework: "unknown",
66
+ Router: "none",
67
+ StateManagement: []string{},
68
+ DataFetching: []string{},
69
+ Databases: []string{},
70
+ RawDependencies: map[string]string{},
71
+ }
72
+ }
73
+
74
+ // ── Stats / Errors / Result ──
75
+
76
+ type Stats struct {
77
+ TotalFiles int `json:"totalFiles"`
78
+ TotalNodes int `json:"totalNodes"`
79
+ SkippedFiles int `json:"skippedFiles"`
80
+ }
81
+
82
+ type ExtractorError struct {
83
+ File string `json:"file,omitempty"`
84
+ Error string `json:"error"`
85
+ }
86
+
87
+ type ExtractorResult struct {
88
+ Fingerprint *Fingerprint `json:"fingerprint"`
89
+ Nodes []map[string]any `json:"nodes"`
90
+ Edges []map[string]any `json:"edges"`
91
+ Routes []map[string]any `json:"routes"`
92
+ Stats Stats `json:"stats"`
93
+ Errors []ExtractorError `json:"errors"`
94
+ }
95
+
96
+ // ── Node / edge builders ──
97
+
98
+ // fileNode — FILE (or TEST) node. Id format: file::rel/path.go (matches
99
+ // src/parser/index.ts). File nodes parent themselves.
100
+ func fileNode(relPath string, endLine int, nodeType string) map[string]any {
101
+ return map[string]any{
102
+ "id": "file::" + relPath,
103
+ "name": path.Base(relPath),
104
+ "type": nodeType,
105
+ "filePath": relPath,
106
+ "startLine": 1,
107
+ "endLine": endLine,
108
+ "parentFile": "file::" + relPath,
109
+ "metadata": map[string]any{
110
+ "nodeCount": 0,
111
+ "childNodeIds": []any{},
112
+ "language": "go",
113
+ },
114
+ }
115
+ }
116
+
117
+ // codeNode — FUNCTION/METHOD/STRUCT/INTERFACE/ENUM/PACKAGE/THIRD_PARTY/ROUTE.
118
+ // rawCode on every code node (summarizer input — engine discards after).
119
+ func codeNode(id, relPath, name, nodeType string, startLine, endLine int, rawCode string, metadata map[string]any) map[string]any {
120
+ return map[string]any{
121
+ "id": id,
122
+ "name": name,
123
+ "type": nodeType,
124
+ "filePath": relPath,
125
+ "startLine": startLine,
126
+ "endLine": endLine,
127
+ "parentFile": "file::" + relPath,
128
+ "codeHash": codeHash(rawCode),
129
+ "rawCode": rawCode,
130
+ "metadata": metadata,
131
+ }
132
+ }
133
+
134
+ func edge(from, to, etype string) map[string]any {
135
+ return map[string]any{"from": from, "to": to, "type": etype}
136
+ }
137
+
138
+ func edgeWithMeta(from, to, etype string, metadata map[string]any) map[string]any {
139
+ return map[string]any{"from": from, "to": to, "type": etype, "metadata": metadata}
140
+ }
@@ -0,0 +1,161 @@
1
+ // extractor.go — pipeline orchestration.
2
+ //
3
+ // Order (mirrors the python/java extractors):
4
+ // walk → parse (facts) → typeload (Mode A/B) → lookup → imports (+PACKAGE
5
+ // nodes) → calls → routes → orm → inheritance → tests → enrich →
6
+ // dedupe → deterministic sort → result.
7
+ // Edge detectors resolve from facts via lookup maps — never re-walk ASTs.
8
+
9
+ package main
10
+
11
+ import (
12
+ "sort"
13
+ )
14
+
15
+ func runExtractor(repoPath string, opts *Options) *ExtractorResult {
16
+ if opts == nil {
17
+ opts = &Options{}
18
+ }
19
+ result := &ExtractorResult{
20
+ Fingerprint: newFingerprint(),
21
+ Nodes: []map[string]any{},
22
+ Edges: []map[string]any{},
23
+ Routes: []map[string]any{},
24
+ Errors: []ExtractorError{},
25
+ }
26
+ addError := func(file, msg string) {
27
+ result.Errors = append(result.Errors, ExtractorError{File: file, Error: msg})
28
+ }
29
+
30
+ // ── fingerprint (go.mod only; net-http/sql upgrades come after walk) ──
31
+ mf := parseGoMod(repoPath)
32
+ fp := fingerprintFromMod(mf)
33
+ result.Fingerprint = fp
34
+
35
+ // ── walk + parse ──
36
+ parsed := parseRepo(repoPath, mf, addError)
37
+
38
+ // stdlib database/sql + net-http presence upgrade the fingerprint
39
+ // (both are stdlib — invisible in go.mod requires).
40
+ if fp.Framework == "unknown" && importsNetHTTP(parsed) {
41
+ fp.Framework = "net-http"
42
+ fp.ProjectType = "backend"
43
+ }
44
+ if importsDatabaseSQL(parsed) && !contains(fp.Databases, "sql") {
45
+ fp.Databases = append(fp.Databases, "sql")
46
+ }
47
+ if hasMainPackage(parsed) {
48
+ fp.ProjectType = "backend"
49
+ }
50
+
51
+ // ── type load (in-process go/types; local + std, external degraded) ──
52
+ ti := loadTypes(parsed, addError)
53
+
54
+ // ── lookup maps — ONE shared index, consumed by all detectors ──
55
+ lookup := buildLookupMaps(parsed, ti)
56
+
57
+ // ── edges, in dependency order ──
58
+ allEdges := []map[string]any{}
59
+ nodes := []map[string]any{}
60
+ routes := []map[string]any{}
61
+
62
+ // file + test nodes first (parents exist before children)
63
+ files := collectFileNodes(parsed, lookup)
64
+ nodes = append(nodes, files...)
65
+
66
+ // IMPORTS edges + [mod] third-party nodes (no PACKAGE nodes — connected-only
67
+ // pruning is engine-side; imports target the package's FILES, JS parity)
68
+ allEdges = append(allEdges, detectImports(parsed, lookup, opts, fp)...)
69
+
70
+ // code nodes (functions/methods/structs/interfaces/enums)
71
+ codeNodes := collectCodeNodes(parsed, lookup, ti)
72
+ nodes = append(nodes, codeNodes...)
73
+
74
+ // CALLS edges
75
+ allEdges = append(allEdges, detectCalls(parsed, ti, lookup, opts)...)
76
+
77
+ // ROUTE nodes + HANDLES edges + BackendRouteNode entries
78
+ routeNodes, handlesEdges, backendRoutes := detectRoutes(parsed, lookup, ti, fp)
79
+ nodes = append(nodes, routeNodes...)
80
+ allEdges = append(allEdges, handlesEdges...)
81
+ routes = append(routes, backendRoutes...)
82
+
83
+ // EXTENDS / IMPLEMENTS
84
+ allEdges = append(allEdges, detectInheritance(parsed, lookup, ti, opts)...)
85
+
86
+ // READS_FROM / WRITES_TO
87
+ allEdges = append(allEdges, detectOrmEdges(parsed, lookup, ti, opts)...)
88
+
89
+ // TESTS edges
90
+ allEdges = append(allEdges, detectTests(parsed, lookup)...)
91
+
92
+ // third-party nodes (gated) — created during imports/calls, collected last
93
+ nodes = append(nodes, lookup.thirdPartyNodes...)
94
+
95
+ // ── dedupe + deterministic sort ──
96
+ allEdges = dedupeEdges(allEdges)
97
+ sort.SliceStable(allEdges, func(i, j int) bool {
98
+ a, b := allEdges[i], allEdges[j]
99
+ return edgeKey(a) < edgeKey(b)
100
+ })
101
+ sort.SliceStable(nodes, func(i, j int) bool {
102
+ return nodeKey(nodes[i]) < nodeKey(nodes[j])
103
+ })
104
+ sort.SliceStable(routes, func(i, j int) bool {
105
+ return routeKey(routes[i]) < routeKey(routes[j])
106
+ })
107
+ // keep errors in insertion order (they're already deterministic: file walk order)
108
+
109
+ result.Nodes = nodes
110
+ result.Edges = allEdges
111
+ result.Routes = routes
112
+ result.Stats = Stats{
113
+ TotalFiles: len(parsed.files),
114
+ TotalNodes: len(nodes),
115
+ SkippedFiles: parsed.skipped,
116
+ }
117
+ return result
118
+ }
119
+
120
+ // ── deterministic key helpers ──
121
+
122
+ func nodeKey(n map[string]any) string {
123
+ s, _ := n["id"].(string)
124
+ return s
125
+ }
126
+
127
+ func edgeKey(e map[string]any) string {
128
+ from, _ := e["from"].(string)
129
+ to, _ := e["to"].(string)
130
+ t, _ := e["type"].(string)
131
+ return from + "\x00" + t + "\x00" + to
132
+ }
133
+
134
+ func routeKey(r map[string]any) string {
135
+ m, _ := r["method"].(string)
136
+ p, _ := r["path"].(string)
137
+ return m + "\x00" + p
138
+ }
139
+
140
+ func dedupeEdges(edges []map[string]any) []map[string]any {
141
+ seen := map[string]bool{}
142
+ out := []map[string]any{}
143
+ for _, e := range edges {
144
+ k := edgeKey(e)
145
+ if seen[k] {
146
+ continue
147
+ }
148
+ seen[k] = true
149
+ out = append(out, e)
150
+ }
151
+ return out
152
+ }
153
+
154
+ func contains(list []string, s string) bool {
155
+ for _, v := range list {
156
+ if v == s {
157
+ return true
158
+ }
159
+ }
160
+ return false
161
+ }