@optave/codegraph 3.1.3 → 3.1.5

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 (232) hide show
  1. package/README.md +38 -84
  2. package/package.json +13 -8
  3. package/src/ast-analysis/engine.js +32 -12
  4. package/src/ast-analysis/shared.js +6 -5
  5. package/src/cli/commands/ast.js +22 -0
  6. package/src/cli/commands/audit.js +45 -0
  7. package/src/cli/commands/batch.js +68 -0
  8. package/src/cli/commands/branch-compare.js +21 -0
  9. package/src/cli/commands/build.js +26 -0
  10. package/src/cli/commands/cfg.js +26 -0
  11. package/src/cli/commands/check.js +74 -0
  12. package/src/cli/commands/children.js +28 -0
  13. package/src/cli/commands/co-change.js +67 -0
  14. package/src/cli/commands/communities.js +19 -0
  15. package/src/cli/commands/complexity.js +46 -0
  16. package/src/cli/commands/context.js +30 -0
  17. package/src/cli/commands/cycles.js +32 -0
  18. package/src/cli/commands/dataflow.js +28 -0
  19. package/src/cli/commands/deps.js +12 -0
  20. package/src/cli/commands/diff-impact.js +26 -0
  21. package/src/cli/commands/embed.js +30 -0
  22. package/src/cli/commands/export.js +78 -0
  23. package/src/cli/commands/exports.js +14 -0
  24. package/src/cli/commands/flow.js +32 -0
  25. package/src/cli/commands/fn-impact.js +26 -0
  26. package/src/cli/commands/impact.js +12 -0
  27. package/src/cli/commands/info.js +76 -0
  28. package/src/cli/commands/map.js +19 -0
  29. package/src/cli/commands/mcp.js +18 -0
  30. package/src/cli/commands/models.js +19 -0
  31. package/src/cli/commands/owners.js +25 -0
  32. package/src/cli/commands/path.js +36 -0
  33. package/src/cli/commands/plot.js +89 -0
  34. package/src/cli/commands/query.js +45 -0
  35. package/src/cli/commands/registry.js +100 -0
  36. package/src/cli/commands/roles.js +30 -0
  37. package/src/cli/commands/search.js +42 -0
  38. package/src/cli/commands/sequence.js +28 -0
  39. package/src/cli/commands/snapshot.js +66 -0
  40. package/src/cli/commands/stats.js +15 -0
  41. package/src/cli/commands/structure.js +33 -0
  42. package/src/cli/commands/triage.js +78 -0
  43. package/src/cli/commands/watch.js +12 -0
  44. package/src/cli/commands/where.js +20 -0
  45. package/src/cli/index.js +124 -0
  46. package/src/cli/shared/open-graph.js +13 -0
  47. package/src/cli/shared/options.js +59 -0
  48. package/src/cli/shared/output.js +1 -0
  49. package/src/cli.js +11 -1522
  50. package/src/db/connection.js +130 -7
  51. package/src/{db.js → db/index.js} +17 -5
  52. package/src/db/migrations.js +42 -1
  53. package/src/db/query-builder.js +20 -12
  54. package/src/db/repository/base.js +201 -0
  55. package/src/db/repository/graph-read.js +7 -4
  56. package/src/db/repository/in-memory-repository.js +575 -0
  57. package/src/db/repository/index.js +5 -1
  58. package/src/db/repository/nodes.js +60 -6
  59. package/src/db/repository/sqlite-repository.js +219 -0
  60. package/src/domain/analysis/context.js +408 -0
  61. package/src/domain/analysis/dependencies.js +341 -0
  62. package/src/domain/analysis/exports.js +134 -0
  63. package/src/domain/analysis/impact.js +466 -0
  64. package/src/domain/analysis/module-map.js +322 -0
  65. package/src/domain/analysis/roles.js +45 -0
  66. package/src/domain/analysis/symbol-lookup.js +238 -0
  67. package/src/domain/graph/builder/context.js +85 -0
  68. package/src/domain/graph/builder/helpers.js +218 -0
  69. package/src/domain/graph/builder/incremental.js +178 -0
  70. package/src/domain/graph/builder/pipeline.js +130 -0
  71. package/src/domain/graph/builder/stages/build-edges.js +297 -0
  72. package/src/domain/graph/builder/stages/build-structure.js +113 -0
  73. package/src/domain/graph/builder/stages/collect-files.js +44 -0
  74. package/src/domain/graph/builder/stages/detect-changes.js +413 -0
  75. package/src/domain/graph/builder/stages/finalize.js +139 -0
  76. package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
  77. package/src/domain/graph/builder/stages/parse-files.js +28 -0
  78. package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
  79. package/src/domain/graph/builder/stages/run-analyses.js +44 -0
  80. package/src/domain/graph/builder.js +11 -0
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/domain/graph/cycles.js +82 -0
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +3 -3
  85. package/src/{watcher.js → domain/graph/watcher.js} +10 -150
  86. package/src/{parser.js → domain/parser.js} +5 -5
  87. package/src/domain/queries.js +48 -0
  88. package/src/domain/search/generator.js +163 -0
  89. package/src/domain/search/index.js +13 -0
  90. package/src/domain/search/models.js +218 -0
  91. package/src/domain/search/search/cli-formatter.js +151 -0
  92. package/src/domain/search/search/filters.js +46 -0
  93. package/src/domain/search/search/hybrid.js +121 -0
  94. package/src/domain/search/search/keyword.js +68 -0
  95. package/src/domain/search/search/prepare.js +66 -0
  96. package/src/domain/search/search/semantic.js +145 -0
  97. package/src/domain/search/stores/fts5.js +27 -0
  98. package/src/domain/search/stores/sqlite-blob.js +24 -0
  99. package/src/domain/search/strategies/source.js +14 -0
  100. package/src/domain/search/strategies/structured.js +43 -0
  101. package/src/domain/search/strategies/text-utils.js +43 -0
  102. package/src/extractors/csharp.js +10 -2
  103. package/src/extractors/go.js +3 -1
  104. package/src/extractors/helpers.js +71 -0
  105. package/src/extractors/java.js +9 -2
  106. package/src/extractors/javascript.js +39 -2
  107. package/src/extractors/php.js +3 -1
  108. package/src/extractors/python.js +14 -3
  109. package/src/extractors/rust.js +3 -1
  110. package/src/{ast.js → features/ast.js} +8 -8
  111. package/src/{audit.js → features/audit.js} +16 -44
  112. package/src/{batch.js → features/batch.js} +6 -5
  113. package/src/{boundaries.js → features/boundaries.js} +2 -2
  114. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  115. package/src/{cfg.js → features/cfg.js} +11 -12
  116. package/src/{check.js → features/check.js} +13 -30
  117. package/src/{cochange.js → features/cochange.js} +5 -5
  118. package/src/{communities.js → features/communities.js} +18 -90
  119. package/src/{complexity.js → features/complexity.js} +13 -13
  120. package/src/{dataflow.js → features/dataflow.js} +12 -13
  121. package/src/features/export.js +378 -0
  122. package/src/{flow.js → features/flow.js} +4 -4
  123. package/src/features/graph-enrichment.js +327 -0
  124. package/src/{manifesto.js → features/manifesto.js} +6 -6
  125. package/src/{owners.js → features/owners.js} +2 -2
  126. package/src/{sequence.js → features/sequence.js} +16 -52
  127. package/src/{snapshot.js → features/snapshot.js} +8 -7
  128. package/src/{structure.js → features/structure.js} +20 -45
  129. package/src/{triage.js → features/triage.js} +27 -79
  130. package/src/graph/algorithms/bfs.js +49 -0
  131. package/src/graph/algorithms/centrality.js +16 -0
  132. package/src/graph/algorithms/index.js +5 -0
  133. package/src/graph/algorithms/louvain.js +26 -0
  134. package/src/graph/algorithms/shortest-path.js +41 -0
  135. package/src/graph/algorithms/tarjan.js +49 -0
  136. package/src/graph/builders/dependency.js +110 -0
  137. package/src/graph/builders/index.js +3 -0
  138. package/src/graph/builders/structure.js +40 -0
  139. package/src/graph/builders/temporal.js +33 -0
  140. package/src/graph/classifiers/index.js +2 -0
  141. package/src/graph/classifiers/risk.js +85 -0
  142. package/src/graph/classifiers/roles.js +64 -0
  143. package/src/graph/index.js +13 -0
  144. package/src/graph/model.js +230 -0
  145. package/src/index.cjs +16 -0
  146. package/src/index.js +42 -219
  147. package/src/{native.js → infrastructure/native.js} +3 -1
  148. package/src/infrastructure/result-formatter.js +2 -21
  149. package/src/mcp/index.js +2 -0
  150. package/src/mcp/middleware.js +26 -0
  151. package/src/mcp/server.js +128 -0
  152. package/src/{mcp.js → mcp/tool-registry.js} +6 -675
  153. package/src/mcp/tools/ast-query.js +14 -0
  154. package/src/mcp/tools/audit.js +21 -0
  155. package/src/mcp/tools/batch-query.js +11 -0
  156. package/src/mcp/tools/branch-compare.js +12 -0
  157. package/src/mcp/tools/cfg.js +21 -0
  158. package/src/mcp/tools/check.js +43 -0
  159. package/src/mcp/tools/co-changes.js +20 -0
  160. package/src/mcp/tools/code-owners.js +12 -0
  161. package/src/mcp/tools/communities.js +15 -0
  162. package/src/mcp/tools/complexity.js +18 -0
  163. package/src/mcp/tools/context.js +17 -0
  164. package/src/mcp/tools/dataflow.js +26 -0
  165. package/src/mcp/tools/diff-impact.js +24 -0
  166. package/src/mcp/tools/execution-flow.js +26 -0
  167. package/src/mcp/tools/export-graph.js +57 -0
  168. package/src/mcp/tools/file-deps.js +12 -0
  169. package/src/mcp/tools/file-exports.js +13 -0
  170. package/src/mcp/tools/find-cycles.js +15 -0
  171. package/src/mcp/tools/fn-impact.js +15 -0
  172. package/src/mcp/tools/impact-analysis.js +12 -0
  173. package/src/mcp/tools/index.js +71 -0
  174. package/src/mcp/tools/list-functions.js +14 -0
  175. package/src/mcp/tools/list-repos.js +11 -0
  176. package/src/mcp/tools/module-map.js +6 -0
  177. package/src/mcp/tools/node-roles.js +14 -0
  178. package/src/mcp/tools/path.js +12 -0
  179. package/src/mcp/tools/query.js +30 -0
  180. package/src/mcp/tools/semantic-search.js +65 -0
  181. package/src/mcp/tools/sequence.js +17 -0
  182. package/src/mcp/tools/structure.js +15 -0
  183. package/src/mcp/tools/symbol-children.js +14 -0
  184. package/src/mcp/tools/triage.js +35 -0
  185. package/src/mcp/tools/where.js +13 -0
  186. package/src/{commands → presentation}/audit.js +2 -2
  187. package/src/{commands → presentation}/batch.js +1 -1
  188. package/src/{commands → presentation}/branch-compare.js +2 -2
  189. package/src/{commands → presentation}/cfg.js +1 -1
  190. package/src/{commands → presentation}/check.js +6 -6
  191. package/src/presentation/colors.js +44 -0
  192. package/src/{commands → presentation}/communities.js +1 -1
  193. package/src/{commands → presentation}/complexity.js +1 -1
  194. package/src/{commands → presentation}/dataflow.js +1 -1
  195. package/src/presentation/export.js +444 -0
  196. package/src/{commands → presentation}/flow.js +2 -2
  197. package/src/{commands → presentation}/manifesto.js +4 -4
  198. package/src/{commands → presentation}/owners.js +1 -1
  199. package/src/presentation/queries-cli/exports.js +46 -0
  200. package/src/presentation/queries-cli/impact.js +198 -0
  201. package/src/presentation/queries-cli/index.js +5 -0
  202. package/src/presentation/queries-cli/inspect.js +334 -0
  203. package/src/presentation/queries-cli/overview.js +197 -0
  204. package/src/presentation/queries-cli/path.js +58 -0
  205. package/src/presentation/queries-cli.js +27 -0
  206. package/src/{commands → presentation}/query.js +1 -1
  207. package/src/presentation/result-formatter.js +144 -0
  208. package/src/presentation/sequence-renderer.js +43 -0
  209. package/src/{commands → presentation}/sequence.js +2 -2
  210. package/src/{commands → presentation}/structure.js +2 -2
  211. package/src/presentation/table.js +47 -0
  212. package/src/{commands → presentation}/triage.js +1 -1
  213. package/src/{viewer.js → presentation/viewer.js} +68 -382
  214. package/src/{constants.js → shared/constants.js} +1 -1
  215. package/src/shared/errors.js +78 -0
  216. package/src/shared/file-utils.js +153 -0
  217. package/src/shared/generators.js +125 -0
  218. package/src/shared/hierarchy.js +27 -0
  219. package/src/shared/normalize.js +59 -0
  220. package/src/builder.js +0 -1486
  221. package/src/cycles.js +0 -137
  222. package/src/embedder.js +0 -1097
  223. package/src/export.js +0 -681
  224. package/src/queries-cli.js +0 -866
  225. package/src/queries.js +0 -2289
  226. /package/src/{config.js → infrastructure/config.js} +0 -0
  227. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  228. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  229. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  230. /package/src/{commands → presentation}/cochange.js +0 -0
  231. /package/src/{kinds.js → shared/kinds.js} +0 -0
  232. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,322 @@
1
+ import path from 'node:path';
2
+ import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js';
3
+ import { isTestFile } from '../../infrastructure/test-filter.js';
4
+ import { findCycles } from '../graph/cycles.js';
5
+ import { LANGUAGE_REGISTRY } from '../parser.js';
6
+
7
+ export const FALSE_POSITIVE_NAMES = new Set([
8
+ 'run',
9
+ 'get',
10
+ 'set',
11
+ 'init',
12
+ 'start',
13
+ 'handle',
14
+ 'main',
15
+ 'new',
16
+ 'create',
17
+ 'update',
18
+ 'delete',
19
+ 'process',
20
+ 'execute',
21
+ 'call',
22
+ 'apply',
23
+ 'setup',
24
+ 'render',
25
+ 'build',
26
+ 'load',
27
+ 'save',
28
+ 'find',
29
+ 'make',
30
+ 'open',
31
+ 'close',
32
+ 'reset',
33
+ 'send',
34
+ 'read',
35
+ 'write',
36
+ ]);
37
+ export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
38
+
39
+ export function moduleMapData(customDbPath, limit = 20, opts = {}) {
40
+ const db = openReadonlyOrFail(customDbPath);
41
+ try {
42
+ const noTests = opts.noTests || false;
43
+
44
+ const testFilter = testFilterSQL('n.file', noTests);
45
+
46
+ const nodes = db
47
+ .prepare(`
48
+ SELECT n.*,
49
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
50
+ (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
51
+ FROM nodes n
52
+ WHERE n.kind = 'file'
53
+ ${testFilter}
54
+ ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
55
+ LIMIT ?
56
+ `)
57
+ .all(limit);
58
+
59
+ const topNodes = nodes.map((n) => ({
60
+ file: n.file,
61
+ dir: path.dirname(n.file) || '.',
62
+ inEdges: n.in_edges,
63
+ outEdges: n.out_edges,
64
+ coupling: n.in_edges + n.out_edges,
65
+ }));
66
+
67
+ const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
68
+ const totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
69
+ const totalFiles = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get().c;
70
+
71
+ return { limit, topNodes, stats: { totalFiles, totalNodes, totalEdges } };
72
+ } finally {
73
+ db.close();
74
+ }
75
+ }
76
+
77
+ export function statsData(customDbPath, opts = {}) {
78
+ const db = openReadonlyOrFail(customDbPath);
79
+ try {
80
+ const noTests = opts.noTests || false;
81
+
82
+ // Build set of test file IDs for filtering nodes and edges
83
+ let testFileIds = null;
84
+ if (noTests) {
85
+ const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all();
86
+ testFileIds = new Set();
87
+ const testFiles = new Set();
88
+ for (const n of allFileNodes) {
89
+ if (isTestFile(n.file)) {
90
+ testFileIds.add(n.id);
91
+ testFiles.add(n.file);
92
+ }
93
+ }
94
+
95
+ // Also collect non-file node IDs that belong to test files
96
+ const allNodes = db.prepare('SELECT id, file FROM nodes').all();
97
+ for (const n of allNodes) {
98
+ if (testFiles.has(n.file)) testFileIds.add(n.id);
99
+ }
100
+ }
101
+
102
+ // Node breakdown by kind
103
+ let nodeRows;
104
+ if (noTests) {
105
+ const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all();
106
+ const filtered = allNodes.filter((n) => !testFileIds.has(n.id));
107
+ const counts = {};
108
+ for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1;
109
+ nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
110
+ } else {
111
+ nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
112
+ }
113
+ const nodesByKind = {};
114
+ let totalNodes = 0;
115
+ for (const r of nodeRows) {
116
+ nodesByKind[r.kind] = r.c;
117
+ totalNodes += r.c;
118
+ }
119
+
120
+ // Edge breakdown by kind
121
+ let edgeRows;
122
+ if (noTests) {
123
+ const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all();
124
+ const filtered = allEdges.filter(
125
+ (e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id),
126
+ );
127
+ const counts = {};
128
+ for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1;
129
+ edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
130
+ } else {
131
+ edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
132
+ }
133
+ const edgesByKind = {};
134
+ let totalEdges = 0;
135
+ for (const r of edgeRows) {
136
+ edgesByKind[r.kind] = r.c;
137
+ totalEdges += r.c;
138
+ }
139
+
140
+ // File/language distribution — map extensions via LANGUAGE_REGISTRY
141
+ const extToLang = new Map();
142
+ for (const entry of LANGUAGE_REGISTRY) {
143
+ for (const ext of entry.extensions) {
144
+ extToLang.set(ext, entry.id);
145
+ }
146
+ }
147
+ let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
148
+ if (noTests) fileNodes = fileNodes.filter((n) => !isTestFile(n.file));
149
+ const byLanguage = {};
150
+ for (const row of fileNodes) {
151
+ const ext = path.extname(row.file).toLowerCase();
152
+ const lang = extToLang.get(ext) || 'other';
153
+ byLanguage[lang] = (byLanguage[lang] || 0) + 1;
154
+ }
155
+ const langCount = Object.keys(byLanguage).length;
156
+
157
+ // Cycles
158
+ const fileCycles = findCycles(db, { fileLevel: true, noTests });
159
+ const fnCycles = findCycles(db, { fileLevel: false, noTests });
160
+
161
+ // Top 5 coupling hotspots (fan-in + fan-out, file nodes)
162
+ const testFilter = testFilterSQL('n.file', noTests);
163
+ const hotspotRows = db
164
+ .prepare(`
165
+ SELECT n.file,
166
+ (SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in,
167
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out
168
+ FROM nodes n
169
+ WHERE n.kind = 'file' ${testFilter}
170
+ ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
171
+ + (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
172
+ `)
173
+ .all();
174
+ const filteredHotspots = noTests ? hotspotRows.filter((r) => !isTestFile(r.file)) : hotspotRows;
175
+ const hotspots = filteredHotspots.slice(0, 5).map((r) => ({
176
+ file: r.file,
177
+ fanIn: r.fan_in,
178
+ fanOut: r.fan_out,
179
+ }));
180
+
181
+ // Embeddings metadata
182
+ let embeddings = null;
183
+ try {
184
+ const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
185
+ if (count && count.c > 0) {
186
+ const meta = {};
187
+ const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
188
+ for (const r of metaRows) meta[r.key] = r.value;
189
+ embeddings = {
190
+ count: count.c,
191
+ model: meta.model || null,
192
+ dim: meta.dim ? parseInt(meta.dim, 10) : null,
193
+ builtAt: meta.built_at || null,
194
+ };
195
+ }
196
+ } catch {
197
+ /* embeddings table may not exist */
198
+ }
199
+
200
+ // Graph quality metrics
201
+ const qualityTestFilter = testFilter.replace(/n\.file/g, 'file');
202
+ const totalCallable = db
203
+ .prepare(
204
+ `SELECT COUNT(*) as c FROM nodes WHERE kind IN ('function', 'method') ${qualityTestFilter}`,
205
+ )
206
+ .get().c;
207
+ const callableWithCallers = db
208
+ .prepare(`
209
+ SELECT COUNT(DISTINCT e.target_id) as c FROM edges e
210
+ JOIN nodes n ON e.target_id = n.id
211
+ WHERE e.kind = 'calls' AND n.kind IN ('function', 'method') ${testFilter}
212
+ `)
213
+ .get().c;
214
+ const callerCoverage = totalCallable > 0 ? callableWithCallers / totalCallable : 0;
215
+
216
+ const totalCallEdges = db
217
+ .prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls'")
218
+ .get().c;
219
+ const highConfCallEdges = db
220
+ .prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls' AND confidence >= 0.7")
221
+ .get().c;
222
+ const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
223
+
224
+ // False-positive warnings: generic names with > threshold callers
225
+ const fpRows = db
226
+ .prepare(`
227
+ SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
228
+ FROM nodes n
229
+ LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
230
+ WHERE n.kind IN ('function', 'method')
231
+ GROUP BY n.id
232
+ HAVING caller_count > ?
233
+ ORDER BY caller_count DESC
234
+ `)
235
+ .all(FALSE_POSITIVE_CALLER_THRESHOLD);
236
+ const falsePositiveWarnings = fpRows
237
+ .filter((r) =>
238
+ FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name),
239
+ )
240
+ .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
241
+
242
+ // Edges from suspicious nodes
243
+ let fpEdgeCount = 0;
244
+ for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
245
+ const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
246
+
247
+ const score = Math.round(
248
+ callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
249
+ );
250
+
251
+ const quality = {
252
+ score,
253
+ callerCoverage: {
254
+ ratio: callerCoverage,
255
+ covered: callableWithCallers,
256
+ total: totalCallable,
257
+ },
258
+ callConfidence: {
259
+ ratio: callConfidence,
260
+ highConf: highConfCallEdges,
261
+ total: totalCallEdges,
262
+ },
263
+ falsePositiveWarnings,
264
+ };
265
+
266
+ // Role distribution
267
+ let roleRows;
268
+ if (noTests) {
269
+ const allRoleNodes = db.prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL').all();
270
+ const filtered = allRoleNodes.filter((n) => !isTestFile(n.file));
271
+ const counts = {};
272
+ for (const n of filtered) counts[n.role] = (counts[n.role] || 0) + 1;
273
+ roleRows = Object.entries(counts).map(([role, c]) => ({ role, c }));
274
+ } else {
275
+ roleRows = db
276
+ .prepare('SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role')
277
+ .all();
278
+ }
279
+ const roles = {};
280
+ for (const r of roleRows) roles[r.role] = r.c;
281
+
282
+ // Complexity summary
283
+ let complexity = null;
284
+ try {
285
+ const cRows = db
286
+ .prepare(
287
+ `SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
288
+ FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
289
+ WHERE n.kind IN ('function','method') ${testFilter}`,
290
+ )
291
+ .all();
292
+ if (cRows.length > 0) {
293
+ const miValues = cRows.map((r) => r.maintainability_index || 0);
294
+ complexity = {
295
+ analyzed: cRows.length,
296
+ avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
297
+ avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
298
+ maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
299
+ maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
300
+ avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
301
+ minMI: +Math.min(...miValues).toFixed(1),
302
+ };
303
+ }
304
+ } catch {
305
+ /* table may not exist in older DBs */
306
+ }
307
+
308
+ return {
309
+ nodes: { total: totalNodes, byKind: nodesByKind },
310
+ edges: { total: totalEdges, byKind: edgesByKind },
311
+ files: { total: fileNodes.length, languages: langCount, byLanguage },
312
+ cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
313
+ hotspots,
314
+ embeddings,
315
+ quality,
316
+ roles,
317
+ complexity,
318
+ };
319
+ } finally {
320
+ db.close();
321
+ }
322
+ }
@@ -0,0 +1,45 @@
1
+ import { openReadonlyOrFail } from '../../db/index.js';
2
+ import { isTestFile } from '../../infrastructure/test-filter.js';
3
+ import { normalizeSymbol } from '../../shared/normalize.js';
4
+ import { paginateResult } from '../../shared/paginate.js';
5
+
6
+ export function rolesData(customDbPath, opts = {}) {
7
+ const db = openReadonlyOrFail(customDbPath);
8
+ try {
9
+ const noTests = opts.noTests || false;
10
+ const filterRole = opts.role || null;
11
+ const filterFile = opts.file || null;
12
+
13
+ const conditions = ['role IS NOT NULL'];
14
+ const params = [];
15
+
16
+ if (filterRole) {
17
+ conditions.push('role = ?');
18
+ params.push(filterRole);
19
+ }
20
+ if (filterFile) {
21
+ conditions.push('file LIKE ?');
22
+ params.push(`%${filterFile}%`);
23
+ }
24
+
25
+ let rows = db
26
+ .prepare(
27
+ `SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
28
+ )
29
+ .all(...params);
30
+
31
+ if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
32
+
33
+ const summary = {};
34
+ for (const r of rows) {
35
+ summary[r.role] = (summary[r.role] || 0) + 1;
36
+ }
37
+
38
+ const hc = new Map();
39
+ const symbols = rows.map((r) => normalizeSymbol(r, db, hc));
40
+ const base = { count: symbols.length, summary, symbols };
41
+ return paginateResult(base, 'symbols', { limit: opts.limit, offset: opts.offset });
42
+ } finally {
43
+ db.close();
44
+ }
45
+ }
@@ -0,0 +1,238 @@
1
+ import {
2
+ countCrossFileCallers,
3
+ findAllIncomingEdges,
4
+ findAllOutgoingEdges,
5
+ findCallers,
6
+ findCrossFileCallTargets,
7
+ findFileNodes,
8
+ findImportSources,
9
+ findImportTargets,
10
+ findNodeChildren,
11
+ findNodesByFile,
12
+ findNodesWithFanIn,
13
+ listFunctionNodes,
14
+ openReadonlyOrFail,
15
+ Repository,
16
+ } from '../../db/index.js';
17
+ import { isTestFile } from '../../infrastructure/test-filter.js';
18
+ import { ALL_SYMBOL_KINDS } from '../../shared/kinds.js';
19
+ import { getFileHash, normalizeSymbol } from '../../shared/normalize.js';
20
+ import { paginateResult } from '../../shared/paginate.js';
21
+
22
+ const FUNCTION_KINDS = ['function', 'method', 'class'];
23
+
24
+ /**
25
+ * Find nodes matching a name query, ranked by relevance.
26
+ * Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
27
+ *
28
+ * @param {object} dbOrRepo - A better-sqlite3 Database or a Repository instance
29
+ */
30
+ export function findMatchingNodes(dbOrRepo, name, opts = {}) {
31
+ const kinds = opts.kind ? [opts.kind] : opts.kinds?.length ? opts.kinds : FUNCTION_KINDS;
32
+
33
+ const isRepo = dbOrRepo instanceof Repository;
34
+ const rows = isRepo
35
+ ? dbOrRepo.findNodesWithFanIn(`%${name}%`, { kinds, file: opts.file })
36
+ : findNodesWithFanIn(dbOrRepo, `%${name}%`, { kinds, file: opts.file });
37
+
38
+ const nodes = opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
39
+
40
+ const lowerQuery = name.toLowerCase();
41
+ for (const node of nodes) {
42
+ const lowerName = node.name.toLowerCase();
43
+ const bareName = lowerName.includes('.') ? lowerName.split('.').pop() : lowerName;
44
+
45
+ let matchScore;
46
+ if (lowerName === lowerQuery || bareName === lowerQuery) {
47
+ matchScore = 100;
48
+ } else if (lowerName.startsWith(lowerQuery) || bareName.startsWith(lowerQuery)) {
49
+ matchScore = 60;
50
+ } else if (lowerName.includes(`.${lowerQuery}`) || lowerName.includes(`${lowerQuery}.`)) {
51
+ matchScore = 40;
52
+ } else {
53
+ matchScore = 10;
54
+ }
55
+
56
+ const fanInBonus = Math.min(Math.log2(node.fan_in + 1) * 5, 25);
57
+ node._relevance = matchScore + fanInBonus;
58
+ }
59
+
60
+ nodes.sort((a, b) => b._relevance - a._relevance);
61
+ return nodes;
62
+ }
63
+
64
+ export function queryNameData(name, customDbPath, opts = {}) {
65
+ const db = openReadonlyOrFail(customDbPath);
66
+ try {
67
+ const noTests = opts.noTests || false;
68
+ let nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`);
69
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
70
+ if (nodes.length === 0) {
71
+ return { query: name, results: [] };
72
+ }
73
+
74
+ const hc = new Map();
75
+ const results = nodes.map((node) => {
76
+ let callees = findAllOutgoingEdges(db, node.id);
77
+
78
+ let callers = findAllIncomingEdges(db, node.id);
79
+
80
+ if (noTests) {
81
+ callees = callees.filter((c) => !isTestFile(c.file));
82
+ callers = callers.filter((c) => !isTestFile(c.file));
83
+ }
84
+
85
+ return {
86
+ ...normalizeSymbol(node, db, hc),
87
+ callees: callees.map((c) => ({
88
+ name: c.name,
89
+ kind: c.kind,
90
+ file: c.file,
91
+ line: c.line,
92
+ edgeKind: c.edge_kind,
93
+ })),
94
+ callers: callers.map((c) => ({
95
+ name: c.name,
96
+ kind: c.kind,
97
+ file: c.file,
98
+ line: c.line,
99
+ edgeKind: c.edge_kind,
100
+ })),
101
+ };
102
+ });
103
+
104
+ const base = { query: name, results };
105
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
106
+ } finally {
107
+ db.close();
108
+ }
109
+ }
110
+
111
+ function whereSymbolImpl(db, target, noTests) {
112
+ const placeholders = ALL_SYMBOL_KINDS.map(() => '?').join(', ');
113
+ let nodes = db
114
+ .prepare(
115
+ `SELECT * FROM nodes WHERE name LIKE ? AND kind IN (${placeholders}) ORDER BY file, line`,
116
+ )
117
+ .all(`%${target}%`, ...ALL_SYMBOL_KINDS);
118
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
119
+
120
+ const hc = new Map();
121
+ return nodes.map((node) => {
122
+ const crossCount = countCrossFileCallers(db, node.id, node.file);
123
+ const exported = crossCount > 0;
124
+
125
+ let uses = findCallers(db, node.id);
126
+ if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
127
+
128
+ return {
129
+ ...normalizeSymbol(node, db, hc),
130
+ exported,
131
+ uses: uses.map((u) => ({ name: u.name, file: u.file, line: u.line })),
132
+ };
133
+ });
134
+ }
135
+
136
+ function whereFileImpl(db, target) {
137
+ const fileNodes = findFileNodes(db, `%${target}%`);
138
+ if (fileNodes.length === 0) return [];
139
+
140
+ return fileNodes.map((fn) => {
141
+ const symbols = findNodesByFile(db, fn.file);
142
+
143
+ const imports = findImportTargets(db, fn.id).map((r) => r.file);
144
+
145
+ const importedBy = findImportSources(db, fn.id).map((r) => r.file);
146
+
147
+ const exportedIds = findCrossFileCallTargets(db, fn.file);
148
+
149
+ const exported = symbols.filter((s) => exportedIds.has(s.id)).map((s) => s.name);
150
+
151
+ return {
152
+ file: fn.file,
153
+ fileHash: getFileHash(db, fn.file),
154
+ symbols: symbols.map((s) => ({ name: s.name, kind: s.kind, line: s.line })),
155
+ imports,
156
+ importedBy,
157
+ exported,
158
+ };
159
+ });
160
+ }
161
+
162
+ export function whereData(target, customDbPath, opts = {}) {
163
+ const db = openReadonlyOrFail(customDbPath);
164
+ try {
165
+ const noTests = opts.noTests || false;
166
+ const fileMode = opts.file || false;
167
+
168
+ const results = fileMode ? whereFileImpl(db, target) : whereSymbolImpl(db, target, noTests);
169
+
170
+ const base = { target, mode: fileMode ? 'file' : 'symbol', results };
171
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
172
+ } finally {
173
+ db.close();
174
+ }
175
+ }
176
+
177
+ export function listFunctionsData(customDbPath, opts = {}) {
178
+ const db = openReadonlyOrFail(customDbPath);
179
+ try {
180
+ const noTests = opts.noTests || false;
181
+
182
+ let rows = listFunctionNodes(db, { file: opts.file, pattern: opts.pattern });
183
+
184
+ if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
185
+
186
+ const hc = new Map();
187
+ const functions = rows.map((r) => normalizeSymbol(r, db, hc));
188
+ const base = { count: functions.length, functions };
189
+ return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
190
+ } finally {
191
+ db.close();
192
+ }
193
+ }
194
+
195
+ export function childrenData(name, customDbPath, opts = {}) {
196
+ const db = openReadonlyOrFail(customDbPath);
197
+ try {
198
+ const noTests = opts.noTests || false;
199
+
200
+ const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
201
+ if (nodes.length === 0) {
202
+ return { name, results: [] };
203
+ }
204
+
205
+ const results = nodes.map((node) => {
206
+ let children;
207
+ try {
208
+ children = findNodeChildren(db, node.id);
209
+ } catch {
210
+ children = [];
211
+ }
212
+ if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
213
+ return {
214
+ name: node.name,
215
+ kind: node.kind,
216
+ file: node.file,
217
+ line: node.line,
218
+ scope: node.scope || null,
219
+ visibility: node.visibility || null,
220
+ qualifiedName: node.qualified_name || null,
221
+ children: children.map((c) => ({
222
+ name: c.name,
223
+ kind: c.kind,
224
+ line: c.line,
225
+ endLine: c.end_line || null,
226
+ qualifiedName: c.qualified_name || null,
227
+ scope: c.scope || null,
228
+ visibility: c.visibility || null,
229
+ })),
230
+ };
231
+ });
232
+
233
+ const base = { name, results };
234
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
235
+ } finally {
236
+ db.close();
237
+ }
238
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * PipelineContext — shared mutable state threaded through all build stages.
3
+ *
4
+ * Each stage reads what it needs and writes what it produces.
5
+ * This replaces the closure-captured locals in the old monolithic buildGraph().
6
+ */
7
+ export class PipelineContext {
8
+ // ── Inputs (set during setup) ──────────────────────────────────────
9
+ /** @type {string} Absolute root directory */
10
+ rootDir;
11
+ /** @type {import('better-sqlite3').Database} */
12
+ db;
13
+ /** @type {string} Absolute path to the database file */
14
+ dbPath;
15
+ /** @type {object} From loadConfig() */
16
+ config;
17
+ /** @type {object} Original buildGraph opts */
18
+ opts;
19
+ /** @type {{ engine: string, dataflow: boolean, ast: boolean }} */
20
+ engineOpts;
21
+ /** @type {string} 'native' | 'wasm' */
22
+ engineName;
23
+ /** @type {string|null} */
24
+ engineVersion;
25
+ /** @type {{ baseUrl: string|null, paths: object }} */
26
+ aliases;
27
+ /** @type {boolean} Whether incremental mode is enabled */
28
+ incremental;
29
+ /** @type {boolean} Force full rebuild (engine/schema mismatch) */
30
+ forceFullRebuild = false;
31
+ /** @type {number} Current schema version */
32
+ schemaVersion;
33
+
34
+ // ── File collection (set by collectFiles stage) ────────────────────
35
+ /** @type {string[]} Absolute file paths */
36
+ allFiles;
37
+ /** @type {Set<string>} Absolute directory paths */
38
+ discoveredDirs;
39
+
40
+ // ── Change detection (set by detectChanges stage) ──────────────────
41
+ /** @type {boolean} */
42
+ isFullBuild;
43
+ /** @type {Array<{ file: string, relPath?: string, content?: string, hash?: string, stat?: object, _reverseDepOnly?: boolean }>} */
44
+ parseChanges;
45
+ /** @type {Array<{ relPath: string, hash: string, stat: object }>} Metadata-only self-heal updates */
46
+ metadataUpdates;
47
+ /** @type {string[]} Relative paths of deleted files */
48
+ removed;
49
+ /** @type {boolean} True when no changes detected — skip remaining stages */
50
+ earlyExit = false;
51
+
52
+ // ── Parsing (set by parseFiles stage) ──────────────────────────────
53
+ /** @type {Map<string, object>} relPath → symbols from parseFilesAuto */
54
+ allSymbols;
55
+ /** @type {Map<string, object>} relPath → symbols (includes incrementally loaded) */
56
+ fileSymbols;
57
+ /** @type {Array<{ file: string, relPath?: string }>} Files to parse this build */
58
+ filesToParse;
59
+
60
+ // ── Import resolution (set by resolveImports stage) ────────────────
61
+ /** @type {Map<string, string>|null} "absFile|source" → resolved path */
62
+ batchResolved;
63
+ /** @type {Map<string, Array>} relPath → re-export descriptors */
64
+ reexportMap;
65
+ /** @type {Set<string>} Files loaded only for barrel resolution (don't rebuild edges) */
66
+ barrelOnlyFiles;
67
+
68
+ // ── Node lookup (set by insertNodes / buildEdges stages) ───────────
69
+ /** @type {Map<string, Array>} name → node rows */
70
+ nodesByName;
71
+ /** @type {Map<string, Array>} "name|file" → node rows */
72
+ nodesByNameAndFile;
73
+
74
+ // ── Misc state ─────────────────────────────────────────────────────
75
+ /** @type {boolean} Whether embeddings table exists */
76
+ hasEmbeddings = false;
77
+ /** @type {Map<string, number>} relPath → line count */
78
+ lineCountMap;
79
+
80
+ // ── Phase timing ───────────────────────────────────────────────────
81
+ timing = {};
82
+
83
+ /** @type {number} performance.now() at build start */
84
+ buildStart;
85
+ }