@optave/codegraph 3.1.5 → 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 (91) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +252 -258
  4. package/src/ast-analysis/shared.js +0 -12
  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 +2 -1
  9. package/src/cli/commands/audit.js +2 -1
  10. package/src/cli/commands/batch.js +2 -1
  11. package/src/cli/commands/brief.js +12 -0
  12. package/src/cli/commands/cfg.js +2 -1
  13. package/src/cli/commands/check.js +20 -23
  14. package/src/cli/commands/children.js +6 -1
  15. package/src/cli/commands/complexity.js +2 -1
  16. package/src/cli/commands/context.js +6 -1
  17. package/src/cli/commands/dataflow.js +2 -1
  18. package/src/cli/commands/deps.js +8 -3
  19. package/src/cli/commands/flow.js +2 -1
  20. package/src/cli/commands/fn-impact.js +6 -1
  21. package/src/cli/commands/owners.js +4 -2
  22. package/src/cli/commands/query.js +6 -1
  23. package/src/cli/commands/roles.js +2 -1
  24. package/src/cli/commands/search.js +8 -2
  25. package/src/cli/commands/sequence.js +2 -1
  26. package/src/cli/commands/triage.js +38 -27
  27. package/src/db/connection.js +18 -12
  28. package/src/db/migrations.js +41 -64
  29. package/src/db/query-builder.js +60 -4
  30. package/src/db/repository/in-memory-repository.js +27 -16
  31. package/src/db/repository/nodes.js +8 -10
  32. package/src/domain/analysis/brief.js +155 -0
  33. package/src/domain/analysis/context.js +174 -190
  34. package/src/domain/analysis/dependencies.js +200 -146
  35. package/src/domain/analysis/exports.js +3 -2
  36. package/src/domain/analysis/impact.js +267 -152
  37. package/src/domain/analysis/module-map.js +247 -221
  38. package/src/domain/analysis/roles.js +8 -5
  39. package/src/domain/analysis/symbol-lookup.js +7 -5
  40. package/src/domain/graph/builder/helpers.js +1 -1
  41. package/src/domain/graph/builder/incremental.js +116 -90
  42. package/src/domain/graph/builder/pipeline.js +106 -80
  43. package/src/domain/graph/builder/stages/build-edges.js +318 -239
  44. package/src/domain/graph/builder/stages/detect-changes.js +198 -177
  45. package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
  46. package/src/domain/graph/watcher.js +2 -2
  47. package/src/domain/parser.js +20 -11
  48. package/src/domain/queries.js +1 -0
  49. package/src/domain/search/search/filters.js +9 -5
  50. package/src/domain/search/search/keyword.js +12 -5
  51. package/src/domain/search/search/prepare.js +13 -5
  52. package/src/extractors/csharp.js +224 -207
  53. package/src/extractors/go.js +176 -172
  54. package/src/extractors/hcl.js +94 -78
  55. package/src/extractors/java.js +213 -207
  56. package/src/extractors/javascript.js +274 -304
  57. package/src/extractors/php.js +234 -221
  58. package/src/extractors/python.js +252 -250
  59. package/src/extractors/ruby.js +192 -185
  60. package/src/extractors/rust.js +182 -167
  61. package/src/features/ast.js +5 -3
  62. package/src/features/audit.js +4 -2
  63. package/src/features/boundaries.js +98 -83
  64. package/src/features/cfg.js +134 -143
  65. package/src/features/communities.js +68 -53
  66. package/src/features/complexity.js +143 -132
  67. package/src/features/dataflow.js +146 -149
  68. package/src/features/export.js +3 -3
  69. package/src/features/graph-enrichment.js +2 -2
  70. package/src/features/manifesto.js +9 -6
  71. package/src/features/owners.js +4 -3
  72. package/src/features/sequence.js +152 -141
  73. package/src/features/shared/find-nodes.js +31 -0
  74. package/src/features/structure.js +130 -99
  75. package/src/features/triage.js +83 -68
  76. package/src/graph/classifiers/risk.js +3 -2
  77. package/src/graph/classifiers/roles.js +6 -3
  78. package/src/index.js +1 -0
  79. package/src/mcp/server.js +65 -56
  80. package/src/mcp/tool-registry.js +13 -0
  81. package/src/mcp/tools/brief.js +8 -0
  82. package/src/mcp/tools/index.js +2 -0
  83. package/src/presentation/brief.js +51 -0
  84. package/src/presentation/queries-cli/exports.js +21 -14
  85. package/src/presentation/queries-cli/impact.js +55 -39
  86. package/src/presentation/queries-cli/inspect.js +184 -189
  87. package/src/presentation/queries-cli/overview.js +57 -58
  88. package/src/presentation/queries-cli/path.js +36 -29
  89. package/src/presentation/table.js +0 -8
  90. package/src/shared/generators.js +7 -3
  91. package/src/shared/kinds.js +1 -1
@@ -13,6 +13,7 @@ import {
13
13
  getComplexityForNode,
14
14
  openReadonlyOrFail,
15
15
  } from '../../db/index.js';
16
+ import { debug } from '../../infrastructure/logger.js';
16
17
  import { isTestFile } from '../../infrastructure/test-filter.js';
17
18
  import {
18
19
  createFileLinesReader,
@@ -26,6 +27,149 @@ import { normalizeSymbol } from '../../shared/normalize.js';
26
27
  import { paginateResult } from '../../shared/paginate.js';
27
28
  import { findMatchingNodes } from './symbol-lookup.js';
28
29
 
30
+ function buildCallees(db, node, repoRoot, getFileLines, opts) {
31
+ const { noTests, depth } = opts;
32
+ const calleeRows = findCallees(db, node.id);
33
+ const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
34
+
35
+ const callees = filteredCallees.map((c) => {
36
+ const cLines = getFileLines(c.file);
37
+ const summary = cLines ? extractSummary(cLines, c.line) : null;
38
+ let calleeSource = null;
39
+ if (depth >= 1) {
40
+ calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
41
+ }
42
+ return {
43
+ name: c.name,
44
+ kind: c.kind,
45
+ file: c.file,
46
+ line: c.line,
47
+ endLine: c.end_line || null,
48
+ summary,
49
+ source: calleeSource,
50
+ };
51
+ });
52
+
53
+ if (depth > 1) {
54
+ const visited = new Set(filteredCallees.map((c) => c.id));
55
+ visited.add(node.id);
56
+ let frontier = filteredCallees.map((c) => c.id);
57
+ const maxDepth = Math.min(depth, 5);
58
+ for (let d = 2; d <= maxDepth; d++) {
59
+ const nextFrontier = [];
60
+ for (const fid of frontier) {
61
+ const deeper = findCallees(db, fid);
62
+ for (const c of deeper) {
63
+ if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
64
+ visited.add(c.id);
65
+ nextFrontier.push(c.id);
66
+ const cLines = getFileLines(c.file);
67
+ callees.push({
68
+ name: c.name,
69
+ kind: c.kind,
70
+ file: c.file,
71
+ line: c.line,
72
+ endLine: c.end_line || null,
73
+ summary: cLines ? extractSummary(cLines, c.line) : null,
74
+ source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
75
+ });
76
+ }
77
+ }
78
+ }
79
+ frontier = nextFrontier;
80
+ if (frontier.length === 0) break;
81
+ }
82
+ }
83
+
84
+ return callees;
85
+ }
86
+
87
+ function buildCallers(db, node, noTests) {
88
+ let callerRows = findCallers(db, node.id);
89
+
90
+ if (node.kind === 'method' && node.name.includes('.')) {
91
+ const methodName = node.name.split('.').pop();
92
+ const relatedMethods = resolveMethodViaHierarchy(db, methodName);
93
+ for (const rm of relatedMethods) {
94
+ if (rm.id === node.id) continue;
95
+ const extraCallers = findCallers(db, rm.id);
96
+ callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
97
+ }
98
+ }
99
+ if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
100
+
101
+ return callerRows.map((c) => ({
102
+ name: c.name,
103
+ kind: c.kind,
104
+ file: c.file,
105
+ line: c.line,
106
+ viaHierarchy: c.viaHierarchy || undefined,
107
+ }));
108
+ }
109
+
110
+ function buildRelatedTests(db, node, getFileLines, includeTests) {
111
+ const testCallerRows = findCallers(db, node.id);
112
+ const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
113
+
114
+ const testsByFile = new Map();
115
+ for (const tc of testCallers) {
116
+ if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
117
+ testsByFile.get(tc.file).push(tc);
118
+ }
119
+
120
+ const relatedTests = [];
121
+ for (const [file] of testsByFile) {
122
+ const tLines = getFileLines(file);
123
+ const testNames = [];
124
+ if (tLines) {
125
+ for (const tl of tLines) {
126
+ const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
127
+ if (tm) testNames.push(tm[1]);
128
+ }
129
+ }
130
+ const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
131
+ relatedTests.push({
132
+ file,
133
+ testCount: testNames.length,
134
+ testNames,
135
+ source: testSource,
136
+ });
137
+ }
138
+
139
+ return relatedTests;
140
+ }
141
+
142
+ function getComplexityMetrics(db, nodeId) {
143
+ try {
144
+ const cRow = getComplexityForNode(db, nodeId);
145
+ if (!cRow) return null;
146
+ return {
147
+ cognitive: cRow.cognitive,
148
+ cyclomatic: cRow.cyclomatic,
149
+ maxNesting: cRow.max_nesting,
150
+ maintainabilityIndex: cRow.maintainability_index || 0,
151
+ halsteadVolume: cRow.halstead_volume || 0,
152
+ };
153
+ } catch (e) {
154
+ debug(`complexity lookup failed for node ${nodeId}: ${e.message}`);
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function getNodeChildrenSafe(db, nodeId) {
160
+ try {
161
+ return findNodeChildren(db, nodeId).map((c) => ({
162
+ name: c.name,
163
+ kind: c.kind,
164
+ line: c.line,
165
+ endLine: c.end_line || null,
166
+ }));
167
+ } catch (e) {
168
+ debug(`findNodeChildren failed for node ${nodeId}: ${e.message}`);
169
+ return [];
170
+ }
171
+ }
172
+
29
173
  function explainFileImpl(db, target, getFileLines) {
30
174
  const fileNodes = findFileNodes(db, `%${target}%`);
31
175
  if (fileNodes.length === 0) return [];
@@ -49,14 +193,10 @@ function explainFileImpl(db, target, getFileLines) {
49
193
  const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
50
194
  const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
51
195
 
52
- // Imports / importedBy
53
196
  const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
54
-
55
197
  const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
56
198
 
57
- // Intra-file data flow
58
199
  const intraEdges = findIntraFileCallEdges(db, fn.file);
59
-
60
200
  const dataFlowMap = new Map();
61
201
  for (const edge of intraEdges) {
62
202
  if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
@@ -67,7 +207,6 @@ function explainFileImpl(db, target, getFileLines) {
67
207
  callees,
68
208
  }));
69
209
 
70
- // Line count: prefer node_metrics (actual), fall back to MAX(end_line)
71
210
  const metric = db
72
211
  .prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`)
73
212
  .get(fn.id);
@@ -95,7 +234,7 @@ function explainFileImpl(db, target, getFileLines) {
95
234
  function explainFunctionImpl(db, target, noTests, getFileLines) {
96
235
  let nodes = db
97
236
  .prepare(
98
- `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module') ORDER BY file, line`,
237
+ `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module','constant') ORDER BY file, line`,
99
238
  )
100
239
  .all(`%${target}%`);
101
240
  if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
@@ -129,29 +268,12 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
129
268
  .filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
130
269
  .map((r) => ({ file: r.file }));
131
270
 
132
- // Complexity metrics
133
- let complexityMetrics = null;
134
- try {
135
- const cRow = getComplexityForNode(db, node.id);
136
- if (cRow) {
137
- complexityMetrics = {
138
- cognitive: cRow.cognitive,
139
- cyclomatic: cRow.cyclomatic,
140
- maxNesting: cRow.max_nesting,
141
- maintainabilityIndex: cRow.maintainability_index || 0,
142
- halsteadVolume: cRow.halstead_volume || 0,
143
- };
144
- }
145
- } catch {
146
- /* table may not exist */
147
- }
148
-
149
271
  return {
150
272
  ...normalizeSymbol(node, db, hc),
151
273
  lineCount,
152
274
  summary,
153
275
  signature,
154
- complexity: complexityMetrics,
276
+ complexity: getComplexityMetrics(db, node.id),
155
277
  callees,
156
278
  callers,
157
279
  relatedTests,
@@ -159,6 +281,28 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
159
281
  });
160
282
  }
161
283
 
284
+ function explainCallees(parentResults, currentDepth, visited, db, noTests, getFileLines) {
285
+ if (currentDepth <= 0) return;
286
+ for (const r of parentResults) {
287
+ const newCallees = [];
288
+ for (const callee of r.callees) {
289
+ const key = `${callee.name}:${callee.file}:${callee.line}`;
290
+ if (visited.has(key)) continue;
291
+ visited.add(key);
292
+ const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
293
+ const exact = calleeResults.find((cr) => cr.file === callee.file && cr.line === callee.line);
294
+ if (exact) {
295
+ exact._depth = (r._depth || 0) + 1;
296
+ newCallees.push(exact);
297
+ }
298
+ }
299
+ if (newCallees.length > 0) {
300
+ r.depDetails = newCallees;
301
+ explainCallees(newCallees, currentDepth - 1, visited, db, noTests, getFileLines);
302
+ }
303
+ }
304
+ }
305
+
162
306
  // ─── Exported functions ──────────────────────────────────────────────────
163
307
 
164
308
  export function contextData(name, customDbPath, opts = {}) {
@@ -177,156 +321,22 @@ export function contextData(name, customDbPath, opts = {}) {
177
321
  return { name, results: [] };
178
322
  }
179
323
 
180
- // No hardcoded slice — pagination handles bounding via limit/offset
181
-
182
324
  const getFileLines = createFileLinesReader(repoRoot);
183
325
 
184
326
  const results = nodes.map((node) => {
185
327
  const fileLines = getFileLines(node.file);
186
328
 
187
- // Source
188
329
  const source = noSource
189
330
  ? null
190
331
  : readSourceRange(repoRoot, node.file, node.line, node.end_line);
191
332
 
192
- // Signature
193
333
  const signature = fileLines ? extractSignature(fileLines, node.line) : null;
194
334
 
195
- // Callees
196
- const calleeRows = findCallees(db, node.id);
197
- const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
198
-
199
- const callees = filteredCallees.map((c) => {
200
- const cLines = getFileLines(c.file);
201
- const summary = cLines ? extractSummary(cLines, c.line) : null;
202
- let calleeSource = null;
203
- if (depth >= 1) {
204
- calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
205
- }
206
- return {
207
- name: c.name,
208
- kind: c.kind,
209
- file: c.file,
210
- line: c.line,
211
- endLine: c.end_line || null,
212
- summary,
213
- source: calleeSource,
214
- };
215
- });
216
-
217
- // Deep callee expansion via BFS (depth > 1, capped at 5)
218
- if (depth > 1) {
219
- const visited = new Set(filteredCallees.map((c) => c.id));
220
- visited.add(node.id);
221
- let frontier = filteredCallees.map((c) => c.id);
222
- const maxDepth = Math.min(depth, 5);
223
- for (let d = 2; d <= maxDepth; d++) {
224
- const nextFrontier = [];
225
- for (const fid of frontier) {
226
- const deeper = findCallees(db, fid);
227
- for (const c of deeper) {
228
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
229
- visited.add(c.id);
230
- nextFrontier.push(c.id);
231
- const cLines = getFileLines(c.file);
232
- callees.push({
233
- name: c.name,
234
- kind: c.kind,
235
- file: c.file,
236
- line: c.line,
237
- endLine: c.end_line || null,
238
- summary: cLines ? extractSummary(cLines, c.line) : null,
239
- source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
240
- });
241
- }
242
- }
243
- }
244
- frontier = nextFrontier;
245
- if (frontier.length === 0) break;
246
- }
247
- }
248
-
249
- // Callers
250
- let callerRows = findCallers(db, node.id);
251
-
252
- // Method hierarchy resolution
253
- if (node.kind === 'method' && node.name.includes('.')) {
254
- const methodName = node.name.split('.').pop();
255
- const relatedMethods = resolveMethodViaHierarchy(db, methodName);
256
- for (const rm of relatedMethods) {
257
- if (rm.id === node.id) continue;
258
- const extraCallers = findCallers(db, rm.id);
259
- callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
260
- }
261
- }
262
- if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
263
-
264
- const callers = callerRows.map((c) => ({
265
- name: c.name,
266
- kind: c.kind,
267
- file: c.file,
268
- line: c.line,
269
- viaHierarchy: c.viaHierarchy || undefined,
270
- }));
271
-
272
- // Related tests: callers that live in test files
273
- const testCallerRows = findCallers(db, node.id);
274
- const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
275
-
276
- const testsByFile = new Map();
277
- for (const tc of testCallers) {
278
- if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
279
- testsByFile.get(tc.file).push(tc);
280
- }
281
-
282
- const relatedTests = [];
283
- for (const [file] of testsByFile) {
284
- const tLines = getFileLines(file);
285
- const testNames = [];
286
- if (tLines) {
287
- for (const tl of tLines) {
288
- const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
289
- if (tm) testNames.push(tm[1]);
290
- }
291
- }
292
- const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
293
- relatedTests.push({
294
- file,
295
- testCount: testNames.length,
296
- testNames,
297
- source: testSource,
298
- });
299
- }
300
-
301
- // Complexity metrics
302
- let complexityMetrics = null;
303
- try {
304
- const cRow = getComplexityForNode(db, node.id);
305
- if (cRow) {
306
- complexityMetrics = {
307
- cognitive: cRow.cognitive,
308
- cyclomatic: cRow.cyclomatic,
309
- maxNesting: cRow.max_nesting,
310
- maintainabilityIndex: cRow.maintainability_index || 0,
311
- halsteadVolume: cRow.halstead_volume || 0,
312
- };
313
- }
314
- } catch {
315
- /* table may not exist */
316
- }
317
-
318
- // Children (parameters, properties, constants)
319
- let nodeChildren = [];
320
- try {
321
- nodeChildren = findNodeChildren(db, node.id).map((c) => ({
322
- name: c.name,
323
- kind: c.kind,
324
- line: c.line,
325
- endLine: c.end_line || null,
326
- }));
327
- } catch {
328
- /* parent_id column may not exist */
329
- }
335
+ const callees = buildCallees(db, node, repoRoot, getFileLines, { noTests, depth });
336
+ const callers = buildCallers(db, node, noTests);
337
+ const relatedTests = buildRelatedTests(db, node, getFileLines, includeTests);
338
+ const complexityMetrics = getComplexityMetrics(db, node.id);
339
+ const nodeChildren = getNodeChildrenSafe(db, node.id);
330
340
 
331
341
  return {
332
342
  name: node.name,
@@ -369,35 +379,9 @@ export function explainData(target, customDbPath, opts = {}) {
369
379
  ? explainFileImpl(db, target, getFileLines)
370
380
  : explainFunctionImpl(db, target, noTests, getFileLines);
371
381
 
372
- // Recursive dependency explanation for function targets
373
382
  if (kind === 'function' && depth > 0 && results.length > 0) {
374
383
  const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
375
-
376
- function explainCallees(parentResults, currentDepth) {
377
- if (currentDepth <= 0) return;
378
- for (const r of parentResults) {
379
- const newCallees = [];
380
- for (const callee of r.callees) {
381
- const key = `${callee.name}:${callee.file}:${callee.line}`;
382
- if (visited.has(key)) continue;
383
- visited.add(key);
384
- const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
385
- const exact = calleeResults.find(
386
- (cr) => cr.file === callee.file && cr.line === callee.line,
387
- );
388
- if (exact) {
389
- exact._depth = (r._depth || 0) + 1;
390
- newCallees.push(exact);
391
- }
392
- }
393
- if (newCallees.length > 0) {
394
- r.depDetails = newCallees;
395
- explainCallees(newCallees, currentDepth - 1);
396
- }
397
- }
398
- }
399
-
400
- explainCallees(results, depth);
384
+ explainCallees(results, depth, visited, db, noTests, getFileLines);
401
385
  }
402
386
 
403
387
  const base = { target, kind, results };