@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
@@ -1,322 +0,0 @@
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
- }
@@ -1,130 +0,0 @@
1
- /**
2
- * Pipeline orchestrator — runs build stages sequentially through a shared PipelineContext.
3
- *
4
- * This is the heart of the builder refactor (ROADMAP 3.9): the monolithic buildGraph()
5
- * is decomposed into independently testable stages that communicate via PipelineContext.
6
- */
7
- import path from 'node:path';
8
- import { performance } from 'node:perf_hooks';
9
- import { loadConfig } from '../config.js';
10
- import { closeDb, getBuildMeta, initSchema, MIGRATIONS, openDb } from '../db.js';
11
- import { info } from '../logger.js';
12
- import { getActiveEngine } from '../parser.js';
13
- import { PipelineContext } from './context.js';
14
- import { loadPathAliases } from './helpers.js';
15
- import { buildEdges } from './stages/build-edges.js';
16
- import { buildStructure } from './stages/build-structure.js';
17
- // Pipeline stages
18
- import { collectFiles } from './stages/collect-files.js';
19
- import { detectChanges } from './stages/detect-changes.js';
20
- import { finalize } from './stages/finalize.js';
21
- import { insertNodes } from './stages/insert-nodes.js';
22
- import { parseFiles } from './stages/parse-files.js';
23
- import { resolveImports } from './stages/resolve-imports.js';
24
- import { runAnalyses } from './stages/run-analyses.js';
25
-
26
- /**
27
- * Build the dependency graph for a codebase.
28
- *
29
- * Signature and return value are identical to the original monolithic buildGraph().
30
- *
31
- * @param {string} rootDir - Root directory to scan
32
- * @param {object} [opts] - Build options
33
- * @returns {Promise<{ phases: object } | undefined>}
34
- */
35
- export async function buildGraph(rootDir, opts = {}) {
36
- const ctx = new PipelineContext();
37
- ctx.buildStart = performance.now();
38
- ctx.opts = opts;
39
-
40
- // ── Setup (creates DB, loads config, selects engine) ──────────────
41
- ctx.rootDir = path.resolve(rootDir);
42
- ctx.dbPath = path.join(ctx.rootDir, '.codegraph', 'graph.db');
43
- ctx.db = openDb(ctx.dbPath);
44
- try {
45
- initSchema(ctx.db);
46
-
47
- ctx.config = loadConfig(ctx.rootDir);
48
- ctx.incremental =
49
- opts.incremental !== false && ctx.config.build && ctx.config.build.incremental !== false;
50
-
51
- ctx.engineOpts = {
52
- engine: opts.engine || 'auto',
53
- dataflow: opts.dataflow !== false,
54
- ast: opts.ast !== false,
55
- };
56
- const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
57
- ctx.engineName = engineName;
58
- ctx.engineVersion = engineVersion;
59
- info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
60
-
61
- // Engine/schema mismatch detection
62
- ctx.schemaVersion = MIGRATIONS[MIGRATIONS.length - 1].version;
63
- ctx.forceFullRebuild = false;
64
- if (ctx.incremental) {
65
- const prevEngine = getBuildMeta(ctx.db, 'engine');
66
- if (prevEngine && prevEngine !== engineName) {
67
- info(`Engine changed (${prevEngine} → ${engineName}), promoting to full rebuild.`);
68
- ctx.forceFullRebuild = true;
69
- }
70
- const prevSchema = getBuildMeta(ctx.db, 'schema_version');
71
- if (prevSchema && Number(prevSchema) !== ctx.schemaVersion) {
72
- info(
73
- `Schema version changed (${prevSchema} → ${ctx.schemaVersion}), promoting to full rebuild.`,
74
- );
75
- ctx.forceFullRebuild = true;
76
- }
77
- }
78
-
79
- // Path aliases
80
- ctx.aliases = loadPathAliases(ctx.rootDir);
81
- if (ctx.config.aliases) {
82
- for (const [key, value] of Object.entries(ctx.config.aliases)) {
83
- const pattern = key.endsWith('/') ? `${key}*` : key;
84
- const target = path.resolve(ctx.rootDir, value);
85
- ctx.aliases.paths[pattern] = [target.endsWith('/') ? `${target}*` : `${target}/*`];
86
- }
87
- }
88
- if (ctx.aliases.baseUrl || Object.keys(ctx.aliases.paths).length > 0) {
89
- info(
90
- `Loaded path aliases: baseUrl=${ctx.aliases.baseUrl || 'none'}, ${Object.keys(ctx.aliases.paths).length} path mappings`,
91
- );
92
- }
93
-
94
- ctx.timing.setupMs = performance.now() - ctx.buildStart;
95
-
96
- // ── Pipeline stages ─────────────────────────────────────────────
97
- await collectFiles(ctx);
98
- await detectChanges(ctx);
99
-
100
- if (ctx.earlyExit) return;
101
-
102
- await parseFiles(ctx);
103
- await insertNodes(ctx);
104
- await resolveImports(ctx);
105
- await buildEdges(ctx);
106
- await buildStructure(ctx);
107
- await runAnalyses(ctx);
108
- await finalize(ctx);
109
- } catch (err) {
110
- if (!ctx.earlyExit) closeDb(ctx.db);
111
- throw err;
112
- }
113
-
114
- return {
115
- phases: {
116
- setupMs: +ctx.timing.setupMs.toFixed(1),
117
- parseMs: +(ctx.timing.parseMs ?? 0).toFixed(1),
118
- insertMs: +(ctx.timing.insertMs ?? 0).toFixed(1),
119
- resolveMs: +(ctx.timing.resolveMs ?? 0).toFixed(1),
120
- edgesMs: +(ctx.timing.edgesMs ?? 0).toFixed(1),
121
- structureMs: +(ctx.timing.structureMs ?? 0).toFixed(1),
122
- rolesMs: +(ctx.timing.rolesMs ?? 0).toFixed(1),
123
- astMs: +(ctx.timing.astMs ?? 0).toFixed(1),
124
- complexityMs: +(ctx.timing.complexityMs ?? 0).toFixed(1),
125
- ...(ctx.timing.cfgMs != null && { cfgMs: +ctx.timing.cfgMs.toFixed(1) }),
126
- ...(ctx.timing.dataflowMs != null && { dataflowMs: +ctx.timing.dataflowMs.toFixed(1) }),
127
- finalizeMs: +(ctx.timing.finalizeMs ?? 0).toFixed(1),
128
- },
129
- };
130
- }