@optave/codegraph 3.1.4 → 3.2.0

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 (210) hide show
  1. package/README.md +29 -72
  2. package/package.json +10 -8
  3. package/src/ast-analysis/engine.js +260 -246
  4. package/src/ast-analysis/shared.js +2 -14
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +4 -7
  9. package/src/cli/commands/audit.js +11 -11
  10. package/src/cli/commands/batch.js +6 -5
  11. package/src/cli/commands/branch-compare.js +1 -1
  12. package/src/cli/commands/brief.js +12 -0
  13. package/src/cli/commands/build.js +1 -1
  14. package/src/cli/commands/cfg.js +5 -8
  15. package/src/cli/commands/check.js +28 -36
  16. package/src/cli/commands/children.js +9 -7
  17. package/src/cli/commands/co-change.js +5 -3
  18. package/src/cli/commands/communities.js +2 -6
  19. package/src/cli/commands/complexity.js +5 -3
  20. package/src/cli/commands/context.js +9 -8
  21. package/src/cli/commands/cycles.js +12 -8
  22. package/src/cli/commands/dataflow.js +5 -8
  23. package/src/cli/commands/deps.js +9 -8
  24. package/src/cli/commands/diff-impact.js +2 -6
  25. package/src/cli/commands/embed.js +1 -1
  26. package/src/cli/commands/export.js +34 -31
  27. package/src/cli/commands/exports.js +2 -6
  28. package/src/cli/commands/flow.js +5 -8
  29. package/src/cli/commands/fn-impact.js +9 -8
  30. package/src/cli/commands/impact.js +2 -6
  31. package/src/cli/commands/info.js +2 -2
  32. package/src/cli/commands/map.js +1 -1
  33. package/src/cli/commands/mcp.js +1 -1
  34. package/src/cli/commands/models.js +1 -1
  35. package/src/cli/commands/owners.js +5 -3
  36. package/src/cli/commands/path.js +2 -2
  37. package/src/cli/commands/plot.js +40 -31
  38. package/src/cli/commands/query.js +9 -8
  39. package/src/cli/commands/registry.js +2 -2
  40. package/src/cli/commands/roles.js +5 -8
  41. package/src/cli/commands/search.js +9 -3
  42. package/src/cli/commands/sequence.js +5 -8
  43. package/src/cli/commands/snapshot.js +6 -1
  44. package/src/cli/commands/stats.js +1 -1
  45. package/src/cli/commands/structure.js +5 -4
  46. package/src/cli/commands/triage.js +41 -30
  47. package/src/cli/commands/watch.js +1 -1
  48. package/src/cli/commands/where.js +2 -6
  49. package/src/cli/index.js +11 -5
  50. package/src/cli/shared/open-graph.js +13 -0
  51. package/src/cli/shared/options.js +22 -2
  52. package/src/cli.js +1 -1
  53. package/src/db/connection.js +140 -11
  54. package/src/{db.js → db/index.js} +12 -5
  55. package/src/db/migrations.js +42 -65
  56. package/src/db/query-builder.js +72 -9
  57. package/src/db/repository/base.js +1 -1
  58. package/src/db/repository/graph-read.js +3 -3
  59. package/src/db/repository/in-memory-repository.js +30 -28
  60. package/src/db/repository/nodes.js +10 -17
  61. package/src/domain/analysis/brief.js +155 -0
  62. package/src/domain/analysis/context.js +392 -0
  63. package/src/domain/analysis/dependencies.js +395 -0
  64. package/src/{analysis → domain/analysis}/exports.js +11 -6
  65. package/src/domain/analysis/impact.js +581 -0
  66. package/src/domain/analysis/module-map.js +348 -0
  67. package/src/{analysis → domain/analysis}/roles.js +12 -9
  68. package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
  69. package/src/{builder → domain/graph/builder}/helpers.js +4 -4
  70. package/src/{builder → domain/graph/builder}/incremental.js +119 -93
  71. package/src/domain/graph/builder/pipeline.js +156 -0
  72. package/src/domain/graph/builder/stages/build-edges.js +376 -0
  73. package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
  74. package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
  75. package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
  76. package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
  77. package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
  78. package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
  79. package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
  80. package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/{cycles.js → domain/graph/cycles.js} +4 -4
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +2 -2
  85. package/src/{watcher.js → domain/graph/watcher.js} +7 -7
  86. package/src/{parser.js → domain/parser.js} +24 -15
  87. package/src/{queries.js → domain/queries.js} +17 -16
  88. package/src/{embeddings → domain/search}/generator.js +3 -3
  89. package/src/{embeddings → domain/search}/models.js +2 -2
  90. package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
  91. package/src/{embeddings → domain/search}/search/filters.js +9 -5
  92. package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
  93. package/src/{embeddings → domain/search}/search/keyword.js +13 -6
  94. package/src/{embeddings → domain/search}/search/prepare.js +15 -7
  95. package/src/{embeddings → domain/search}/search/semantic.js +1 -1
  96. package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
  97. package/src/extractors/csharp.js +224 -207
  98. package/src/extractors/go.js +176 -172
  99. package/src/extractors/hcl.js +94 -78
  100. package/src/extractors/java.js +213 -207
  101. package/src/extractors/javascript.js +275 -305
  102. package/src/extractors/php.js +234 -221
  103. package/src/extractors/python.js +252 -250
  104. package/src/extractors/ruby.js +192 -185
  105. package/src/extractors/rust.js +182 -167
  106. package/src/{ast.js → features/ast.js} +13 -11
  107. package/src/{audit.js → features/audit.js} +20 -46
  108. package/src/{batch.js → features/batch.js} +5 -5
  109. package/src/{boundaries.js → features/boundaries.js} +100 -85
  110. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  111. package/src/{cfg.js → features/cfg.js} +141 -150
  112. package/src/{check.js → features/check.js} +13 -30
  113. package/src/{cochange.js → features/cochange.js} +5 -5
  114. package/src/{communities.js → features/communities.js} +72 -57
  115. package/src/{complexity.js → features/complexity.js} +154 -143
  116. package/src/{dataflow.js → features/dataflow.js} +155 -158
  117. package/src/{export.js → features/export.js} +6 -6
  118. package/src/{flow.js → features/flow.js} +4 -4
  119. package/src/{viewer.js → features/graph-enrichment.js} +8 -8
  120. package/src/{manifesto.js → features/manifesto.js} +15 -12
  121. package/src/{owners.js → features/owners.js} +6 -5
  122. package/src/features/sequence.js +300 -0
  123. package/src/features/shared/find-nodes.js +31 -0
  124. package/src/{snapshot.js → features/snapshot.js} +3 -3
  125. package/src/{structure.js → features/structure.js} +139 -108
  126. package/src/features/triage.js +141 -0
  127. package/src/graph/builders/dependency.js +33 -14
  128. package/src/graph/classifiers/risk.js +3 -2
  129. package/src/graph/classifiers/roles.js +6 -3
  130. package/src/index.cjs +16 -0
  131. package/src/index.js +40 -39
  132. package/src/{native.js → infrastructure/native.js} +1 -1
  133. package/src/mcp/middleware.js +1 -1
  134. package/src/mcp/server.js +68 -59
  135. package/src/mcp/tool-registry.js +15 -2
  136. package/src/mcp/tools/ast-query.js +1 -1
  137. package/src/mcp/tools/audit.js +1 -1
  138. package/src/mcp/tools/batch-query.js +1 -1
  139. package/src/mcp/tools/branch-compare.js +3 -1
  140. package/src/mcp/tools/brief.js +8 -0
  141. package/src/mcp/tools/cfg.js +1 -1
  142. package/src/mcp/tools/check.js +3 -3
  143. package/src/mcp/tools/co-changes.js +1 -1
  144. package/src/mcp/tools/code-owners.js +1 -1
  145. package/src/mcp/tools/communities.js +1 -1
  146. package/src/mcp/tools/complexity.js +1 -1
  147. package/src/mcp/tools/dataflow.js +2 -2
  148. package/src/mcp/tools/execution-flow.js +2 -2
  149. package/src/mcp/tools/export-graph.js +2 -2
  150. package/src/mcp/tools/find-cycles.js +2 -2
  151. package/src/mcp/tools/index.js +2 -0
  152. package/src/mcp/tools/list-repos.js +1 -1
  153. package/src/mcp/tools/sequence.js +1 -1
  154. package/src/mcp/tools/structure.js +1 -1
  155. package/src/mcp/tools/triage.js +2 -2
  156. package/src/{commands → presentation}/audit.js +2 -2
  157. package/src/{commands → presentation}/batch.js +1 -1
  158. package/src/{commands → presentation}/branch-compare.js +2 -2
  159. package/src/presentation/brief.js +51 -0
  160. package/src/{commands → presentation}/cfg.js +1 -1
  161. package/src/{commands → presentation}/check.js +2 -2
  162. package/src/{commands → presentation}/communities.js +1 -1
  163. package/src/{commands → presentation}/complexity.js +1 -1
  164. package/src/{commands → presentation}/dataflow.js +1 -1
  165. package/src/{commands → presentation}/flow.js +2 -2
  166. package/src/{commands → presentation}/manifesto.js +1 -1
  167. package/src/{commands → presentation}/owners.js +1 -1
  168. package/src/presentation/queries-cli/exports.js +53 -0
  169. package/src/presentation/queries-cli/impact.js +214 -0
  170. package/src/presentation/queries-cli/index.js +5 -0
  171. package/src/presentation/queries-cli/inspect.js +329 -0
  172. package/src/presentation/queries-cli/overview.js +196 -0
  173. package/src/presentation/queries-cli/path.js +65 -0
  174. package/src/presentation/queries-cli.js +27 -0
  175. package/src/{commands → presentation}/query.js +1 -1
  176. package/src/presentation/result-formatter.js +126 -3
  177. package/src/{commands → presentation}/sequence.js +2 -2
  178. package/src/{commands → presentation}/structure.js +1 -1
  179. package/src/presentation/table.js +0 -8
  180. package/src/{commands → presentation}/triage.js +1 -1
  181. package/src/{constants.js → shared/constants.js} +1 -1
  182. package/src/shared/file-utils.js +2 -2
  183. package/src/shared/generators.js +9 -5
  184. package/src/shared/hierarchy.js +1 -1
  185. package/src/{kinds.js → shared/kinds.js} +1 -1
  186. package/src/analysis/context.js +0 -408
  187. package/src/analysis/dependencies.js +0 -341
  188. package/src/analysis/impact.js +0 -463
  189. package/src/analysis/module-map.js +0 -322
  190. package/src/builder/pipeline.js +0 -130
  191. package/src/builder/stages/build-edges.js +0 -297
  192. package/src/builder/stages/insert-nodes.js +0 -195
  193. package/src/mcp.js +0 -2
  194. package/src/queries-cli.js +0 -866
  195. package/src/sequence.js +0 -289
  196. package/src/triage.js +0 -126
  197. /package/src/{builder → domain/graph/builder}/context.js +0 -0
  198. /package/src/{builder.js → domain/graph/builder.js} +0 -0
  199. /package/src/{embeddings → domain/search}/index.js +0 -0
  200. /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
  201. /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
  202. /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
  203. /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
  204. /package/src/{config.js → infrastructure/config.js} +0 -0
  205. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  206. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  207. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  208. /package/src/{commands → presentation}/cochange.js +0 -0
  209. /package/src/{errors.js → shared/errors.js} +0 -0
  210. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,348 @@
1
+ import path from 'node:path';
2
+ import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js';
3
+ import { debug } from '../../infrastructure/logger.js';
4
+ import { isTestFile } from '../../infrastructure/test-filter.js';
5
+ import { findCycles } from '../graph/cycles.js';
6
+ import { LANGUAGE_REGISTRY } from '../parser.js';
7
+
8
+ export const FALSE_POSITIVE_NAMES = new Set([
9
+ 'run',
10
+ 'get',
11
+ 'set',
12
+ 'init',
13
+ 'start',
14
+ 'handle',
15
+ 'main',
16
+ 'new',
17
+ 'create',
18
+ 'update',
19
+ 'delete',
20
+ 'process',
21
+ 'execute',
22
+ 'call',
23
+ 'apply',
24
+ 'setup',
25
+ 'render',
26
+ 'build',
27
+ 'load',
28
+ 'save',
29
+ 'find',
30
+ 'make',
31
+ 'open',
32
+ 'close',
33
+ 'reset',
34
+ 'send',
35
+ 'read',
36
+ 'write',
37
+ ]);
38
+ export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Section helpers
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function buildTestFileIds(db) {
45
+ const allFileNodes = db.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all();
46
+ const testFileIds = new Set();
47
+ const testFiles = new Set();
48
+ for (const n of allFileNodes) {
49
+ if (isTestFile(n.file)) {
50
+ testFileIds.add(n.id);
51
+ testFiles.add(n.file);
52
+ }
53
+ }
54
+ const allNodes = db.prepare('SELECT id, file FROM nodes').all();
55
+ for (const n of allNodes) {
56
+ if (testFiles.has(n.file)) testFileIds.add(n.id);
57
+ }
58
+ return testFileIds;
59
+ }
60
+
61
+ function countNodesByKind(db, testFileIds) {
62
+ let nodeRows;
63
+ if (testFileIds) {
64
+ const allNodes = db.prepare('SELECT id, kind, file FROM nodes').all();
65
+ const filtered = allNodes.filter((n) => !testFileIds.has(n.id));
66
+ const counts = {};
67
+ for (const n of filtered) counts[n.kind] = (counts[n.kind] || 0) + 1;
68
+ nodeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
69
+ } else {
70
+ nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
71
+ }
72
+ const byKind = {};
73
+ let total = 0;
74
+ for (const r of nodeRows) {
75
+ byKind[r.kind] = r.c;
76
+ total += r.c;
77
+ }
78
+ return { total, byKind };
79
+ }
80
+
81
+ function countEdgesByKind(db, testFileIds) {
82
+ let edgeRows;
83
+ if (testFileIds) {
84
+ const allEdges = db.prepare('SELECT source_id, target_id, kind FROM edges').all();
85
+ const filtered = allEdges.filter(
86
+ (e) => !testFileIds.has(e.source_id) && !testFileIds.has(e.target_id),
87
+ );
88
+ const counts = {};
89
+ for (const e of filtered) counts[e.kind] = (counts[e.kind] || 0) + 1;
90
+ edgeRows = Object.entries(counts).map(([kind, c]) => ({ kind, c }));
91
+ } else {
92
+ edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
93
+ }
94
+ const byKind = {};
95
+ let total = 0;
96
+ for (const r of edgeRows) {
97
+ byKind[r.kind] = r.c;
98
+ total += r.c;
99
+ }
100
+ return { total, byKind };
101
+ }
102
+
103
+ function countFilesByLanguage(db, noTests) {
104
+ const extToLang = new Map();
105
+ for (const entry of LANGUAGE_REGISTRY) {
106
+ for (const ext of entry.extensions) {
107
+ extToLang.set(ext, entry.id);
108
+ }
109
+ }
110
+ let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
111
+ if (noTests) fileNodes = fileNodes.filter((n) => !isTestFile(n.file));
112
+ const byLanguage = {};
113
+ for (const row of fileNodes) {
114
+ const ext = path.extname(row.file).toLowerCase();
115
+ const lang = extToLang.get(ext) || 'other';
116
+ byLanguage[lang] = (byLanguage[lang] || 0) + 1;
117
+ }
118
+ return { total: fileNodes.length, languages: Object.keys(byLanguage).length, byLanguage };
119
+ }
120
+
121
+ function findHotspots(db, noTests, limit) {
122
+ const testFilter = testFilterSQL('n.file', noTests);
123
+ const hotspotRows = db
124
+ .prepare(`
125
+ SELECT n.file,
126
+ (SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in,
127
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out
128
+ FROM nodes n
129
+ WHERE n.kind = 'file' ${testFilter}
130
+ ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
131
+ + (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
132
+ `)
133
+ .all();
134
+ const filtered = noTests ? hotspotRows.filter((r) => !isTestFile(r.file)) : hotspotRows;
135
+ return filtered.slice(0, limit).map((r) => ({
136
+ file: r.file,
137
+ fanIn: r.fan_in,
138
+ fanOut: r.fan_out,
139
+ }));
140
+ }
141
+
142
+ function getEmbeddingsInfo(db) {
143
+ try {
144
+ const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
145
+ if (count && count.c > 0) {
146
+ const meta = {};
147
+ const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
148
+ for (const r of metaRows) meta[r.key] = r.value;
149
+ return {
150
+ count: count.c,
151
+ model: meta.model || null,
152
+ dim: meta.dim ? parseInt(meta.dim, 10) : null,
153
+ builtAt: meta.built_at || null,
154
+ };
155
+ }
156
+ } catch (e) {
157
+ debug(`embeddings lookup skipped: ${e.message}`);
158
+ }
159
+ return null;
160
+ }
161
+
162
+ function computeQualityMetrics(db, testFilter) {
163
+ const qualityTestFilter = testFilter.replace(/n\.file/g, 'file');
164
+
165
+ const totalCallable = db
166
+ .prepare(
167
+ `SELECT COUNT(*) as c FROM nodes WHERE kind IN ('function', 'method') ${qualityTestFilter}`,
168
+ )
169
+ .get().c;
170
+ const callableWithCallers = db
171
+ .prepare(`
172
+ SELECT COUNT(DISTINCT e.target_id) as c FROM edges e
173
+ JOIN nodes n ON e.target_id = n.id
174
+ WHERE e.kind = 'calls' AND n.kind IN ('function', 'method') ${testFilter}
175
+ `)
176
+ .get().c;
177
+ const callerCoverage = totalCallable > 0 ? callableWithCallers / totalCallable : 0;
178
+
179
+ const totalCallEdges = db.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls'").get().c;
180
+ const highConfCallEdges = db
181
+ .prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls' AND confidence >= 0.7")
182
+ .get().c;
183
+ const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
184
+
185
+ const fpRows = db
186
+ .prepare(`
187
+ SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
188
+ FROM nodes n
189
+ LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
190
+ WHERE n.kind IN ('function', 'method')
191
+ GROUP BY n.id
192
+ HAVING caller_count > ?
193
+ ORDER BY caller_count DESC
194
+ `)
195
+ .all(FALSE_POSITIVE_CALLER_THRESHOLD);
196
+ const falsePositiveWarnings = fpRows
197
+ .filter((r) =>
198
+ FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name),
199
+ )
200
+ .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
201
+
202
+ let fpEdgeCount = 0;
203
+ for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
204
+ const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
205
+
206
+ const score = Math.round(
207
+ callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
208
+ );
209
+
210
+ return {
211
+ score,
212
+ callerCoverage: {
213
+ ratio: callerCoverage,
214
+ covered: callableWithCallers,
215
+ total: totalCallable,
216
+ },
217
+ callConfidence: {
218
+ ratio: callConfidence,
219
+ highConf: highConfCallEdges,
220
+ total: totalCallEdges,
221
+ },
222
+ falsePositiveWarnings,
223
+ };
224
+ }
225
+
226
+ function countRoles(db, noTests) {
227
+ let roleRows;
228
+ if (noTests) {
229
+ const allRoleNodes = db.prepare('SELECT role, file FROM nodes WHERE role IS NOT NULL').all();
230
+ const filtered = allRoleNodes.filter((n) => !isTestFile(n.file));
231
+ const counts = {};
232
+ for (const n of filtered) counts[n.role] = (counts[n.role] || 0) + 1;
233
+ roleRows = Object.entries(counts).map(([role, c]) => ({ role, c }));
234
+ } else {
235
+ roleRows = db
236
+ .prepare('SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role')
237
+ .all();
238
+ }
239
+ const roles = {};
240
+ for (const r of roleRows) roles[r.role] = r.c;
241
+ return roles;
242
+ }
243
+
244
+ function getComplexitySummary(db, testFilter) {
245
+ try {
246
+ const cRows = db
247
+ .prepare(
248
+ `SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
249
+ FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
250
+ WHERE n.kind IN ('function','method') ${testFilter}`,
251
+ )
252
+ .all();
253
+ if (cRows.length > 0) {
254
+ const miValues = cRows.map((r) => r.maintainability_index || 0);
255
+ return {
256
+ analyzed: cRows.length,
257
+ avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
258
+ avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
259
+ maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
260
+ maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
261
+ avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
262
+ minMI: +Math.min(...miValues).toFixed(1),
263
+ };
264
+ }
265
+ } catch (e) {
266
+ debug(`complexity summary skipped: ${e.message}`);
267
+ }
268
+ return null;
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Public API
273
+ // ---------------------------------------------------------------------------
274
+
275
+ export function moduleMapData(customDbPath, limit = 20, opts = {}) {
276
+ const db = openReadonlyOrFail(customDbPath);
277
+ try {
278
+ const noTests = opts.noTests || false;
279
+
280
+ const testFilter = testFilterSQL('n.file', noTests);
281
+
282
+ const nodes = db
283
+ .prepare(`
284
+ SELECT n.*,
285
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
286
+ (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
287
+ FROM nodes n
288
+ WHERE n.kind = 'file'
289
+ ${testFilter}
290
+ ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
291
+ LIMIT ?
292
+ `)
293
+ .all(limit);
294
+
295
+ const topNodes = nodes.map((n) => ({
296
+ file: n.file,
297
+ dir: path.dirname(n.file) || '.',
298
+ inEdges: n.in_edges,
299
+ outEdges: n.out_edges,
300
+ coupling: n.in_edges + n.out_edges,
301
+ }));
302
+
303
+ const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
304
+ const totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
305
+ const totalFiles = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get().c;
306
+
307
+ return { limit, topNodes, stats: { totalFiles, totalNodes, totalEdges } };
308
+ } finally {
309
+ db.close();
310
+ }
311
+ }
312
+
313
+ export function statsData(customDbPath, opts = {}) {
314
+ const db = openReadonlyOrFail(customDbPath);
315
+ try {
316
+ const noTests = opts.noTests || false;
317
+ const testFilter = testFilterSQL('n.file', noTests);
318
+
319
+ const testFileIds = noTests ? buildTestFileIds(db) : null;
320
+
321
+ const { total: totalNodes, byKind: nodesByKind } = countNodesByKind(db, testFileIds);
322
+ const { total: totalEdges, byKind: edgesByKind } = countEdgesByKind(db, testFileIds);
323
+ const files = countFilesByLanguage(db, noTests);
324
+
325
+ const fileCycles = findCycles(db, { fileLevel: true, noTests });
326
+ const fnCycles = findCycles(db, { fileLevel: false, noTests });
327
+
328
+ const hotspots = findHotspots(db, noTests, 5);
329
+ const embeddings = getEmbeddingsInfo(db);
330
+ const quality = computeQualityMetrics(db, testFilter);
331
+ const roles = countRoles(db, noTests);
332
+ const complexity = getComplexitySummary(db, testFilter);
333
+
334
+ return {
335
+ nodes: { total: totalNodes, byKind: nodesByKind },
336
+ edges: { total: totalEdges, byKind: edgesByKind },
337
+ files,
338
+ cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
339
+ hotspots,
340
+ embeddings,
341
+ quality,
342
+ roles,
343
+ complexity,
344
+ };
345
+ } finally {
346
+ db.close();
347
+ }
348
+ }
@@ -1,15 +1,14 @@
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';
1
+ import { openReadonlyOrFail } from '../../db/index.js';
2
+ import { buildFileConditionSQL } from '../../db/query-builder.js';
3
+ import { isTestFile } from '../../infrastructure/test-filter.js';
4
+ import { normalizeSymbol } from '../../shared/normalize.js';
5
+ import { paginateResult } from '../../shared/paginate.js';
5
6
 
6
7
  export function rolesData(customDbPath, opts = {}) {
7
8
  const db = openReadonlyOrFail(customDbPath);
8
9
  try {
9
10
  const noTests = opts.noTests || false;
10
11
  const filterRole = opts.role || null;
11
- const filterFile = opts.file || null;
12
-
13
12
  const conditions = ['role IS NOT NULL'];
14
13
  const params = [];
15
14
 
@@ -17,9 +16,13 @@ export function rolesData(customDbPath, opts = {}) {
17
16
  conditions.push('role = ?');
18
17
  params.push(filterRole);
19
18
  }
20
- if (filterFile) {
21
- conditions.push('file LIKE ?');
22
- params.push(`%${filterFile}%`);
19
+ {
20
+ const fc = buildFileConditionSQL(opts.file, 'file');
21
+ if (fc.sql) {
22
+ // Strip leading ' AND ' since we're using conditions array
23
+ conditions.push(fc.sql.replace(/^ AND /, ''));
24
+ params.push(...fc.params);
25
+ }
23
26
  }
24
27
 
25
28
  let rows = db
@@ -12,22 +12,29 @@ import {
12
12
  findNodesWithFanIn,
13
13
  listFunctionNodes,
14
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';
15
+ Repository,
16
+ } from '../../db/index.js';
17
+ import { debug } from '../../infrastructure/logger.js';
18
+ import { isTestFile } from '../../infrastructure/test-filter.js';
19
+ import { EVERY_SYMBOL_KIND } from '../../shared/kinds.js';
20
+ import { getFileHash, normalizeSymbol } from '../../shared/normalize.js';
21
+ import { paginateResult } from '../../shared/paginate.js';
20
22
 
21
- const FUNCTION_KINDS = ['function', 'method', 'class'];
23
+ const FUNCTION_KINDS = ['function', 'method', 'class', 'constant'];
22
24
 
23
25
  /**
24
26
  * Find nodes matching a name query, ranked by relevance.
25
27
  * Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
28
+ *
29
+ * @param {object} dbOrRepo - A better-sqlite3 Database or a Repository instance
26
30
  */
27
- export function findMatchingNodes(db, name, opts = {}) {
31
+ export function findMatchingNodes(dbOrRepo, name, opts = {}) {
28
32
  const kinds = opts.kind ? [opts.kind] : opts.kinds?.length ? opts.kinds : FUNCTION_KINDS;
29
33
 
30
- const rows = findNodesWithFanIn(db, `%${name}%`, { kinds, file: opts.file });
34
+ const isRepo = dbOrRepo instanceof Repository;
35
+ const rows = isRepo
36
+ ? dbOrRepo.findNodesWithFanIn(`%${name}%`, { kinds, file: opts.file })
37
+ : findNodesWithFanIn(dbOrRepo, `%${name}%`, { kinds, file: opts.file });
31
38
 
32
39
  const nodes = opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
33
40
 
@@ -103,12 +110,12 @@ export function queryNameData(name, customDbPath, opts = {}) {
103
110
  }
104
111
 
105
112
  function whereSymbolImpl(db, target, noTests) {
106
- const placeholders = ALL_SYMBOL_KINDS.map(() => '?').join(', ');
113
+ const placeholders = EVERY_SYMBOL_KIND.map(() => '?').join(', ');
107
114
  let nodes = db
108
115
  .prepare(
109
116
  `SELECT * FROM nodes WHERE name LIKE ? AND kind IN (${placeholders}) ORDER BY file, line`,
110
117
  )
111
- .all(`%${target}%`, ...ALL_SYMBOL_KINDS);
118
+ .all(`%${target}%`, ...EVERY_SYMBOL_KIND);
112
119
  if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
113
120
 
114
121
  const hc = new Map();
@@ -200,7 +207,8 @@ export function childrenData(name, customDbPath, opts = {}) {
200
207
  let children;
201
208
  try {
202
209
  children = findNodeChildren(db, node.id);
203
- } catch {
210
+ } catch (e) {
211
+ debug(`findNodeChildren failed for node ${node.id}: ${e.message}`);
204
212
  children = [];
205
213
  }
206
214
  if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
@@ -6,9 +6,9 @@
6
6
  import { createHash } from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
- import { EXTENSIONS, IGNORE_DIRS } from '../constants.js';
10
- import { purgeFilesData } from '../db.js';
11
- import { warn } from '../logger.js';
9
+ import { purgeFilesData } from '../../../db/index.js';
10
+ import { warn } from '../../../infrastructure/logger.js';
11
+ import { EXTENSIONS, IGNORE_DIRS } from '../../../shared/constants.js';
12
12
 
13
13
  export const BUILTIN_RECEIVERS = new Set([
14
14
  'console',
@@ -179,7 +179,7 @@ export function purgeFilesFromGraph(db, files, options = {}) {
179
179
  }
180
180
 
181
181
  /** Batch INSERT chunk size for multi-value INSERTs. */
182
- export const BATCH_CHUNK = 200;
182
+ const BATCH_CHUNK = 200;
183
183
 
184
184
  /**
185
185
  * Batch-insert node rows via multi-value INSERT statements.