@optave/codegraph 3.1.2 → 3.1.4

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 (194) hide show
  1. package/README.md +19 -21
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1472
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1514
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/complexity.js +1 -1
  76. package/src/cycles.js +30 -85
  77. package/src/dataflow.js +1 -2
  78. package/src/db/connection.js +4 -4
  79. package/src/db/migrations.js +41 -0
  80. package/src/db/query-builder.js +6 -5
  81. package/src/db/repository/base.js +201 -0
  82. package/src/db/repository/cached-stmt.js +19 -0
  83. package/src/db/repository/cfg.js +27 -38
  84. package/src/db/repository/cochange.js +16 -3
  85. package/src/db/repository/complexity.js +11 -6
  86. package/src/db/repository/dataflow.js +6 -1
  87. package/src/db/repository/edges.js +120 -98
  88. package/src/db/repository/embeddings.js +14 -3
  89. package/src/db/repository/graph-read.js +32 -9
  90. package/src/db/repository/in-memory-repository.js +584 -0
  91. package/src/db/repository/index.js +6 -1
  92. package/src/db/repository/nodes.js +110 -40
  93. package/src/db/repository/sqlite-repository.js +219 -0
  94. package/src/db.js +5 -0
  95. package/src/embeddings/generator.js +163 -0
  96. package/src/embeddings/index.js +13 -0
  97. package/src/embeddings/models.js +218 -0
  98. package/src/embeddings/search/cli-formatter.js +151 -0
  99. package/src/embeddings/search/filters.js +46 -0
  100. package/src/embeddings/search/hybrid.js +121 -0
  101. package/src/embeddings/search/keyword.js +68 -0
  102. package/src/embeddings/search/prepare.js +66 -0
  103. package/src/embeddings/search/semantic.js +145 -0
  104. package/src/embeddings/stores/fts5.js +27 -0
  105. package/src/embeddings/stores/sqlite-blob.js +24 -0
  106. package/src/embeddings/strategies/source.js +14 -0
  107. package/src/embeddings/strategies/structured.js +43 -0
  108. package/src/embeddings/strategies/text-utils.js +43 -0
  109. package/src/errors.js +78 -0
  110. package/src/export.js +217 -520
  111. package/src/extractors/csharp.js +10 -2
  112. package/src/extractors/go.js +3 -1
  113. package/src/extractors/helpers.js +71 -0
  114. package/src/extractors/java.js +9 -2
  115. package/src/extractors/javascript.js +38 -1
  116. package/src/extractors/php.js +3 -1
  117. package/src/extractors/python.js +14 -3
  118. package/src/extractors/rust.js +3 -1
  119. package/src/graph/algorithms/bfs.js +49 -0
  120. package/src/graph/algorithms/centrality.js +16 -0
  121. package/src/graph/algorithms/index.js +5 -0
  122. package/src/graph/algorithms/louvain.js +26 -0
  123. package/src/graph/algorithms/shortest-path.js +41 -0
  124. package/src/graph/algorithms/tarjan.js +49 -0
  125. package/src/graph/builders/dependency.js +91 -0
  126. package/src/graph/builders/index.js +3 -0
  127. package/src/graph/builders/structure.js +40 -0
  128. package/src/graph/builders/temporal.js +33 -0
  129. package/src/graph/classifiers/index.js +2 -0
  130. package/src/graph/classifiers/risk.js +85 -0
  131. package/src/graph/classifiers/roles.js +64 -0
  132. package/src/graph/index.js +13 -0
  133. package/src/graph/model.js +230 -0
  134. package/src/index.js +33 -204
  135. package/src/infrastructure/result-formatter.js +2 -21
  136. package/src/mcp/index.js +2 -0
  137. package/src/mcp/middleware.js +26 -0
  138. package/src/mcp/server.js +128 -0
  139. package/src/mcp/tool-registry.js +801 -0
  140. package/src/mcp/tools/ast-query.js +14 -0
  141. package/src/mcp/tools/audit.js +21 -0
  142. package/src/mcp/tools/batch-query.js +11 -0
  143. package/src/mcp/tools/branch-compare.js +10 -0
  144. package/src/mcp/tools/cfg.js +21 -0
  145. package/src/mcp/tools/check.js +43 -0
  146. package/src/mcp/tools/co-changes.js +20 -0
  147. package/src/mcp/tools/code-owners.js +12 -0
  148. package/src/mcp/tools/communities.js +15 -0
  149. package/src/mcp/tools/complexity.js +18 -0
  150. package/src/mcp/tools/context.js +17 -0
  151. package/src/mcp/tools/dataflow.js +26 -0
  152. package/src/mcp/tools/diff-impact.js +24 -0
  153. package/src/mcp/tools/execution-flow.js +26 -0
  154. package/src/mcp/tools/export-graph.js +57 -0
  155. package/src/mcp/tools/file-deps.js +12 -0
  156. package/src/mcp/tools/file-exports.js +13 -0
  157. package/src/mcp/tools/find-cycles.js +15 -0
  158. package/src/mcp/tools/fn-impact.js +15 -0
  159. package/src/mcp/tools/impact-analysis.js +12 -0
  160. package/src/mcp/tools/index.js +71 -0
  161. package/src/mcp/tools/list-functions.js +14 -0
  162. package/src/mcp/tools/list-repos.js +11 -0
  163. package/src/mcp/tools/module-map.js +6 -0
  164. package/src/mcp/tools/node-roles.js +14 -0
  165. package/src/mcp/tools/path.js +12 -0
  166. package/src/mcp/tools/query.js +30 -0
  167. package/src/mcp/tools/semantic-search.js +65 -0
  168. package/src/mcp/tools/sequence.js +17 -0
  169. package/src/mcp/tools/structure.js +15 -0
  170. package/src/mcp/tools/symbol-children.js +14 -0
  171. package/src/mcp/tools/triage.js +35 -0
  172. package/src/mcp/tools/where.js +13 -0
  173. package/src/mcp.js +2 -1470
  174. package/src/native.js +34 -10
  175. package/src/parser.js +53 -2
  176. package/src/presentation/colors.js +44 -0
  177. package/src/presentation/export.js +444 -0
  178. package/src/presentation/result-formatter.js +21 -0
  179. package/src/presentation/sequence-renderer.js +43 -0
  180. package/src/presentation/table.js +47 -0
  181. package/src/presentation/viewer.js +634 -0
  182. package/src/queries.js +35 -2276
  183. package/src/resolve.js +1 -1
  184. package/src/sequence.js +2 -38
  185. package/src/shared/file-utils.js +153 -0
  186. package/src/shared/generators.js +125 -0
  187. package/src/shared/hierarchy.js +27 -0
  188. package/src/shared/normalize.js +59 -0
  189. package/src/snapshot.js +6 -5
  190. package/src/structure.js +15 -40
  191. package/src/triage.js +20 -72
  192. package/src/viewer.js +35 -656
  193. package/src/watcher.js +8 -148
  194. package/src/embedder.js +0 -1097
@@ -0,0 +1,322 @@
1
+ import path from 'node:path';
2
+ import { findCycles } from '../cycles.js';
3
+ import { openReadonlyOrFail, testFilterSQL } from '../db.js';
4
+ import { isTestFile } from '../infrastructure/test-filter.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.js';
2
+ import { isTestFile } from '../infrastructure/test-filter.js';
3
+ import { paginateResult } from '../paginate.js';
4
+ import { normalizeSymbol } from '../shared/normalize.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,232 @@
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
+ } from '../db.js';
16
+ import { isTestFile } from '../infrastructure/test-filter.js';
17
+ import { ALL_SYMBOL_KINDS } from '../kinds.js';
18
+ import { paginateResult } from '../paginate.js';
19
+ import { getFileHash, normalizeSymbol } from '../shared/normalize.js';
20
+
21
+ const FUNCTION_KINDS = ['function', 'method', 'class'];
22
+
23
+ /**
24
+ * Find nodes matching a name query, ranked by relevance.
25
+ * Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
26
+ */
27
+ export function findMatchingNodes(db, name, opts = {}) {
28
+ const kinds = opts.kind ? [opts.kind] : opts.kinds?.length ? opts.kinds : FUNCTION_KINDS;
29
+
30
+ const rows = findNodesWithFanIn(db, `%${name}%`, { kinds, file: opts.file });
31
+
32
+ const nodes = opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
33
+
34
+ const lowerQuery = name.toLowerCase();
35
+ for (const node of nodes) {
36
+ const lowerName = node.name.toLowerCase();
37
+ const bareName = lowerName.includes('.') ? lowerName.split('.').pop() : lowerName;
38
+
39
+ let matchScore;
40
+ if (lowerName === lowerQuery || bareName === lowerQuery) {
41
+ matchScore = 100;
42
+ } else if (lowerName.startsWith(lowerQuery) || bareName.startsWith(lowerQuery)) {
43
+ matchScore = 60;
44
+ } else if (lowerName.includes(`.${lowerQuery}`) || lowerName.includes(`${lowerQuery}.`)) {
45
+ matchScore = 40;
46
+ } else {
47
+ matchScore = 10;
48
+ }
49
+
50
+ const fanInBonus = Math.min(Math.log2(node.fan_in + 1) * 5, 25);
51
+ node._relevance = matchScore + fanInBonus;
52
+ }
53
+
54
+ nodes.sort((a, b) => b._relevance - a._relevance);
55
+ return nodes;
56
+ }
57
+
58
+ export function queryNameData(name, customDbPath, opts = {}) {
59
+ const db = openReadonlyOrFail(customDbPath);
60
+ try {
61
+ const noTests = opts.noTests || false;
62
+ let nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`);
63
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
64
+ if (nodes.length === 0) {
65
+ return { query: name, results: [] };
66
+ }
67
+
68
+ const hc = new Map();
69
+ const results = nodes.map((node) => {
70
+ let callees = findAllOutgoingEdges(db, node.id);
71
+
72
+ let callers = findAllIncomingEdges(db, node.id);
73
+
74
+ if (noTests) {
75
+ callees = callees.filter((c) => !isTestFile(c.file));
76
+ callers = callers.filter((c) => !isTestFile(c.file));
77
+ }
78
+
79
+ return {
80
+ ...normalizeSymbol(node, db, hc),
81
+ callees: callees.map((c) => ({
82
+ name: c.name,
83
+ kind: c.kind,
84
+ file: c.file,
85
+ line: c.line,
86
+ edgeKind: c.edge_kind,
87
+ })),
88
+ callers: callers.map((c) => ({
89
+ name: c.name,
90
+ kind: c.kind,
91
+ file: c.file,
92
+ line: c.line,
93
+ edgeKind: c.edge_kind,
94
+ })),
95
+ };
96
+ });
97
+
98
+ const base = { query: name, results };
99
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
100
+ } finally {
101
+ db.close();
102
+ }
103
+ }
104
+
105
+ function whereSymbolImpl(db, target, noTests) {
106
+ const placeholders = ALL_SYMBOL_KINDS.map(() => '?').join(', ');
107
+ let nodes = db
108
+ .prepare(
109
+ `SELECT * FROM nodes WHERE name LIKE ? AND kind IN (${placeholders}) ORDER BY file, line`,
110
+ )
111
+ .all(`%${target}%`, ...ALL_SYMBOL_KINDS);
112
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
113
+
114
+ const hc = new Map();
115
+ return nodes.map((node) => {
116
+ const crossCount = countCrossFileCallers(db, node.id, node.file);
117
+ const exported = crossCount > 0;
118
+
119
+ let uses = findCallers(db, node.id);
120
+ if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
121
+
122
+ return {
123
+ ...normalizeSymbol(node, db, hc),
124
+ exported,
125
+ uses: uses.map((u) => ({ name: u.name, file: u.file, line: u.line })),
126
+ };
127
+ });
128
+ }
129
+
130
+ function whereFileImpl(db, target) {
131
+ const fileNodes = findFileNodes(db, `%${target}%`);
132
+ if (fileNodes.length === 0) return [];
133
+
134
+ return fileNodes.map((fn) => {
135
+ const symbols = findNodesByFile(db, fn.file);
136
+
137
+ const imports = findImportTargets(db, fn.id).map((r) => r.file);
138
+
139
+ const importedBy = findImportSources(db, fn.id).map((r) => r.file);
140
+
141
+ const exportedIds = findCrossFileCallTargets(db, fn.file);
142
+
143
+ const exported = symbols.filter((s) => exportedIds.has(s.id)).map((s) => s.name);
144
+
145
+ return {
146
+ file: fn.file,
147
+ fileHash: getFileHash(db, fn.file),
148
+ symbols: symbols.map((s) => ({ name: s.name, kind: s.kind, line: s.line })),
149
+ imports,
150
+ importedBy,
151
+ exported,
152
+ };
153
+ });
154
+ }
155
+
156
+ export function whereData(target, customDbPath, opts = {}) {
157
+ const db = openReadonlyOrFail(customDbPath);
158
+ try {
159
+ const noTests = opts.noTests || false;
160
+ const fileMode = opts.file || false;
161
+
162
+ const results = fileMode ? whereFileImpl(db, target) : whereSymbolImpl(db, target, noTests);
163
+
164
+ const base = { target, mode: fileMode ? 'file' : 'symbol', results };
165
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
166
+ } finally {
167
+ db.close();
168
+ }
169
+ }
170
+
171
+ export function listFunctionsData(customDbPath, opts = {}) {
172
+ const db = openReadonlyOrFail(customDbPath);
173
+ try {
174
+ const noTests = opts.noTests || false;
175
+
176
+ let rows = listFunctionNodes(db, { file: opts.file, pattern: opts.pattern });
177
+
178
+ if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
179
+
180
+ const hc = new Map();
181
+ const functions = rows.map((r) => normalizeSymbol(r, db, hc));
182
+ const base = { count: functions.length, functions };
183
+ return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
184
+ } finally {
185
+ db.close();
186
+ }
187
+ }
188
+
189
+ export function childrenData(name, customDbPath, opts = {}) {
190
+ const db = openReadonlyOrFail(customDbPath);
191
+ try {
192
+ const noTests = opts.noTests || false;
193
+
194
+ const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
195
+ if (nodes.length === 0) {
196
+ return { name, results: [] };
197
+ }
198
+
199
+ const results = nodes.map((node) => {
200
+ let children;
201
+ try {
202
+ children = findNodeChildren(db, node.id);
203
+ } catch {
204
+ children = [];
205
+ }
206
+ if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
207
+ return {
208
+ name: node.name,
209
+ kind: node.kind,
210
+ file: node.file,
211
+ line: node.line,
212
+ scope: node.scope || null,
213
+ visibility: node.visibility || null,
214
+ qualifiedName: node.qualified_name || null,
215
+ children: children.map((c) => ({
216
+ name: c.name,
217
+ kind: c.kind,
218
+ line: c.line,
219
+ endLine: c.end_line || null,
220
+ qualifiedName: c.qualified_name || null,
221
+ scope: c.scope || null,
222
+ visibility: c.visibility || null,
223
+ })),
224
+ };
225
+ });
226
+
227
+ const base = { name, results };
228
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
229
+ } finally {
230
+ db.close();
231
+ }
232
+ }
@@ -2,6 +2,7 @@
2
2
  * Shared utilities for AST analysis modules (complexity, CFG, dataflow, AST nodes).
3
3
  */
4
4
 
5
+ import { ConfigError } from '../errors.js';
5
6
  import { LANGUAGE_REGISTRY } from '../parser.js';
6
7
 
7
8
  // ─── Generic Rule Factory ─────────────────────────────────────────────────
@@ -18,7 +19,7 @@ export function makeRules(defaults, overrides, label) {
18
19
  const validKeys = new Set(Object.keys(defaults));
19
20
  for (const key of Object.keys(overrides)) {
20
21
  if (!validKeys.has(key)) {
21
- throw new Error(`${label} rules: unknown key "${key}"`);
22
+ throw new ConfigError(`${label} rules: unknown key "${key}"`);
22
23
  }
23
24
  }
24
25
  return { ...defaults, ...overrides };
@@ -61,10 +62,10 @@ export const CFG_DEFAULTS = {
61
62
  export function makeCfgRules(overrides) {
62
63
  const rules = makeRules(CFG_DEFAULTS, overrides, 'CFG');
63
64
  if (!(rules.functionNodes instanceof Set) || rules.functionNodes.size === 0) {
64
- throw new Error('CFG rules: functionNodes must be a non-empty Set');
65
+ throw new ConfigError('CFG rules: functionNodes must be a non-empty Set');
65
66
  }
66
67
  if (!(rules.forNodes instanceof Set)) {
67
- throw new Error('CFG rules: forNodes must be a Set');
68
+ throw new ConfigError('CFG rules: forNodes must be a Set');
68
69
  }
69
70
  return rules;
70
71
  }
@@ -136,7 +137,7 @@ export const DATAFLOW_DEFAULTS = {
136
137
  export function makeDataflowRules(overrides) {
137
138
  const rules = makeRules(DATAFLOW_DEFAULTS, overrides, 'Dataflow');
138
139
  if (!(rules.functionNodes instanceof Set) || rules.functionNodes.size === 0) {
139
- throw new Error('Dataflow rules: functionNodes must be a non-empty Set');
140
+ throw new ConfigError('Dataflow rules: functionNodes must be a non-empty Set');
140
141
  }
141
142
  return rules;
142
143
  }
package/src/batch.js CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { complexityData } from './complexity.js';
9
9
  import { dataflowData } from './dataflow.js';
10
+ import { ConfigError } from './errors.js';
10
11
  import { flowData } from './flow.js';
11
12
  import {
12
13
  contextData,
@@ -53,7 +54,7 @@ export const BATCH_COMMANDS = {
53
54
  export function batchData(command, targets, customDbPath, opts = {}) {
54
55
  const entry = BATCH_COMMANDS[command];
55
56
  if (!entry) {
56
- throw new Error(
57
+ throw new ConfigError(
57
58
  `Unknown batch command "${command}". Valid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
58
59
  );
59
60
  }