@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
@@ -21,254 +21,280 @@ import {
21
21
  truncate,
22
22
  } from '../visitor-utils.js';
23
23
 
24
- /**
25
- * Create a dataflow visitor for use with walkWithVisitors.
26
- *
27
- * @param {object} rules - DATAFLOW_RULES for the language
28
- * @returns {Visitor}
29
- */
30
- export function createDataflowVisitor(rules) {
31
- const isCallNode = rules.callNodes ? (t) => rules.callNodes.has(t) : (t) => t === rules.callNode;
32
-
33
- const parameters = [];
34
- const returns = [];
35
- const assignments = [];
36
- const argFlows = [];
37
- const mutations = [];
24
+ // ── Scope helpers ───────────────────────────────────────────────────────
38
25
 
39
- const scopeStack = [];
26
+ function currentScope(scopeStack) {
27
+ return scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
28
+ }
40
29
 
41
- function currentScope() {
42
- return scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
30
+ function findBinding(name, scopeStack) {
31
+ for (let i = scopeStack.length - 1; i >= 0; i--) {
32
+ const scope = scopeStack[i];
33
+ if (scope.params.has(name))
34
+ return { type: 'param', index: scope.params.get(name), funcName: scope.funcName };
35
+ if (scope.locals.has(name))
36
+ return { type: 'local', source: scope.locals.get(name), funcName: scope.funcName };
43
37
  }
38
+ return null;
39
+ }
44
40
 
45
- function findBinding(name) {
46
- for (let i = scopeStack.length - 1; i >= 0; i--) {
47
- const scope = scopeStack[i];
48
- if (scope.params.has(name))
49
- return { type: 'param', index: scope.params.get(name), funcName: scope.funcName };
50
- if (scope.locals.has(name))
51
- return { type: 'local', source: scope.locals.get(name), funcName: scope.funcName };
52
- }
53
- return null;
41
+ function bindingConfidence(binding) {
42
+ if (!binding) return 0.5;
43
+ if (binding.type === 'param') return 1.0;
44
+ if (binding.type === 'local') {
45
+ if (binding.source?.type === 'call_return') return 0.9;
46
+ if (binding.source?.type === 'destructured') return 0.8;
47
+ return 0.9;
54
48
  }
49
+ return 0.5;
50
+ }
55
51
 
56
- function bindingConfidence(binding) {
57
- if (!binding) return 0.5;
58
- if (binding.type === 'param') return 1.0;
59
- if (binding.type === 'local') {
60
- if (binding.source?.type === 'call_return') return 0.9;
61
- if (binding.source?.type === 'destructured') return 0.8;
62
- return 0.9;
63
- }
64
- return 0.5;
65
- }
52
+ // ── Node helpers ────────────────────────────────────────────────────────
66
53
 
67
- function unwrapAwait(node) {
68
- if (rules.awaitNode && node.type === rules.awaitNode) {
69
- return node.namedChildren[0] || node;
70
- }
71
- return node;
54
+ function unwrapAwait(node, rules) {
55
+ if (rules.awaitNode && node.type === rules.awaitNode) {
56
+ return node.namedChildren[0] || node;
72
57
  }
58
+ return node;
59
+ }
73
60
 
74
- function isCall(node) {
75
- return node && isCallNode(node.type);
76
- }
61
+ function isCall(node, isCallNode) {
62
+ return node && isCallNode(node.type);
63
+ }
77
64
 
78
- function handleVarDeclarator(node) {
79
- let nameNode = node.childForFieldName(rules.varNameField);
80
- let valueNode = rules.varValueField ? node.childForFieldName(rules.varValueField) : null;
65
+ // ── Node handlers ───────────────────────────────────────────────────────
81
66
 
82
- if (!valueNode && rules.equalsClauseType) {
83
- for (const child of node.namedChildren) {
84
- if (child.type === rules.equalsClauseType) {
85
- valueNode = child.childForFieldName('value') || child.namedChildren[0];
86
- break;
87
- }
88
- }
89
- }
67
+ function handleVarDeclarator(node, rules, scopeStack, assignments, isCallNode) {
68
+ let nameNode = node.childForFieldName(rules.varNameField);
69
+ let valueNode = rules.varValueField ? node.childForFieldName(rules.varValueField) : null;
90
70
 
91
- if (!valueNode) {
92
- for (const child of node.namedChildren) {
93
- if (child !== nameNode && isCall(unwrapAwait(child))) {
94
- valueNode = child;
95
- break;
96
- }
71
+ if (!valueNode && rules.equalsClauseType) {
72
+ for (const child of node.namedChildren) {
73
+ if (child.type === rules.equalsClauseType) {
74
+ valueNode = child.childForFieldName('value') || child.namedChildren[0];
75
+ break;
97
76
  }
98
77
  }
78
+ }
99
79
 
100
- if (rules.expressionListType) {
101
- if (nameNode?.type === rules.expressionListType) nameNode = nameNode.namedChildren[0];
102
- if (valueNode?.type === rules.expressionListType) valueNode = valueNode.namedChildren[0];
80
+ if (!valueNode) {
81
+ for (const child of node.namedChildren) {
82
+ if (child !== nameNode && isCall(unwrapAwait(child, rules), isCallNode)) {
83
+ valueNode = child;
84
+ break;
85
+ }
103
86
  }
87
+ }
104
88
 
105
- const scope = currentScope();
106
- if (!nameNode || !valueNode || !scope) return;
107
-
108
- const unwrapped = unwrapAwait(valueNode);
109
- const callExpr = isCall(unwrapped) ? unwrapped : null;
89
+ if (rules.expressionListType) {
90
+ if (nameNode?.type === rules.expressionListType) nameNode = nameNode.namedChildren[0];
91
+ if (valueNode?.type === rules.expressionListType) valueNode = valueNode.namedChildren[0];
92
+ }
110
93
 
111
- if (callExpr) {
112
- const callee = resolveCalleeName(callExpr, rules);
113
- if (callee && scope.funcName) {
114
- if (
115
- (rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
116
- (rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
117
- ) {
118
- const names = extractParamNames(nameNode, rules);
119
- for (const n of names) {
120
- assignments.push({
121
- varName: n,
122
- callerFunc: scope.funcName,
123
- sourceCallName: callee,
124
- expression: truncate(node.text),
125
- line: node.startPosition.row + 1,
126
- });
127
- scope.locals.set(n, { type: 'destructured', callee });
128
- }
129
- } else {
130
- const varName =
131
- nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
132
- ? nameNode.text
133
- : nameNode.text;
94
+ const scope = currentScope(scopeStack);
95
+ if (!nameNode || !valueNode || !scope) return;
96
+
97
+ const unwrapped = unwrapAwait(valueNode, rules);
98
+ const callExpr = isCall(unwrapped, isCallNode) ? unwrapped : null;
99
+
100
+ if (callExpr) {
101
+ const callee = resolveCalleeName(callExpr, rules);
102
+ if (callee && scope.funcName) {
103
+ if (
104
+ (rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
105
+ (rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
106
+ ) {
107
+ const names = extractParamNames(nameNode, rules);
108
+ for (const n of names) {
134
109
  assignments.push({
135
- varName,
110
+ varName: n,
136
111
  callerFunc: scope.funcName,
137
112
  sourceCallName: callee,
138
113
  expression: truncate(node.text),
139
114
  line: node.startPosition.row + 1,
140
115
  });
141
- scope.locals.set(varName, { type: 'call_return', callee });
116
+ scope.locals.set(n, { type: 'destructured', callee });
142
117
  }
118
+ } else {
119
+ const varName =
120
+ nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
121
+ ? nameNode.text
122
+ : nameNode.text;
123
+ assignments.push({
124
+ varName,
125
+ callerFunc: scope.funcName,
126
+ sourceCallName: callee,
127
+ expression: truncate(node.text),
128
+ line: node.startPosition.row + 1,
129
+ });
130
+ scope.locals.set(varName, { type: 'call_return', callee });
143
131
  }
144
132
  }
145
133
  }
134
+ }
146
135
 
147
- function handleAssignment(node) {
148
- const left = node.childForFieldName(rules.assignLeftField);
149
- const right = node.childForFieldName(rules.assignRightField);
150
- const scope = currentScope();
151
- if (!scope?.funcName) return;
152
-
153
- if (left && rules.memberNode && left.type === rules.memberNode) {
154
- const receiver = memberReceiver(left, rules);
155
- if (receiver) {
156
- const binding = findBinding(receiver);
157
- if (binding) {
158
- mutations.push({
159
- funcName: scope.funcName,
160
- receiverName: receiver,
161
- binding,
162
- mutatingExpr: truncate(node.text),
163
- line: node.startPosition.row + 1,
164
- });
165
- }
136
+ function handleAssignment(node, rules, scopeStack, assignments, mutations, isCallNode) {
137
+ const left = node.childForFieldName(rules.assignLeftField);
138
+ const right = node.childForFieldName(rules.assignRightField);
139
+ const scope = currentScope(scopeStack);
140
+ if (!scope?.funcName) return;
141
+
142
+ if (left && rules.memberNode && left.type === rules.memberNode) {
143
+ const receiver = memberReceiver(left, rules);
144
+ if (receiver) {
145
+ const binding = findBinding(receiver, scopeStack);
146
+ if (binding) {
147
+ mutations.push({
148
+ funcName: scope.funcName,
149
+ receiverName: receiver,
150
+ binding,
151
+ mutatingExpr: truncate(node.text),
152
+ line: node.startPosition.row + 1,
153
+ });
166
154
  }
167
155
  }
156
+ }
168
157
 
169
- if (left && isIdent(left.type, rules) && right) {
170
- const unwrapped = unwrapAwait(right);
171
- const callExpr = isCall(unwrapped) ? unwrapped : null;
172
- if (callExpr) {
173
- const callee = resolveCalleeName(callExpr, rules);
174
- if (callee) {
175
- assignments.push({
176
- varName: left.text,
177
- callerFunc: scope.funcName,
178
- sourceCallName: callee,
179
- expression: truncate(node.text),
180
- line: node.startPosition.row + 1,
181
- });
182
- scope.locals.set(left.text, { type: 'call_return', callee });
183
- }
158
+ if (left && isIdent(left.type, rules) && right) {
159
+ const unwrapped = unwrapAwait(right, rules);
160
+ const callExpr = isCall(unwrapped, isCallNode) ? unwrapped : null;
161
+ if (callExpr) {
162
+ const callee = resolveCalleeName(callExpr, rules);
163
+ if (callee) {
164
+ assignments.push({
165
+ varName: left.text,
166
+ callerFunc: scope.funcName,
167
+ sourceCallName: callee,
168
+ expression: truncate(node.text),
169
+ line: node.startPosition.row + 1,
170
+ });
171
+ scope.locals.set(left.text, { type: 'call_return', callee });
184
172
  }
185
173
  }
186
174
  }
175
+ }
187
176
 
188
- function handleCallExpr(node) {
189
- const callee = resolveCalleeName(node, rules);
190
- const argsNode = node.childForFieldName(rules.callArgsField);
191
- const scope = currentScope();
192
- if (!callee || !argsNode || !scope?.funcName) return;
177
+ function handleCallExpr(node, rules, scopeStack, argFlows) {
178
+ const callee = resolveCalleeName(node, rules);
179
+ const argsNode = node.childForFieldName(rules.callArgsField);
180
+ const scope = currentScope(scopeStack);
181
+ if (!callee || !argsNode || !scope?.funcName) return;
193
182
 
194
- let argIndex = 0;
195
- for (let arg of argsNode.namedChildren) {
196
- if (rules.argumentWrapperType && arg.type === rules.argumentWrapperType) {
197
- arg = arg.namedChildren[0] || arg;
198
- }
199
- const unwrapped =
200
- rules.spreadType && arg.type === rules.spreadType ? arg.namedChildren[0] || arg : arg;
201
- if (!unwrapped) {
202
- argIndex++;
203
- continue;
204
- }
183
+ let argIndex = 0;
184
+ for (let arg of argsNode.namedChildren) {
185
+ if (rules.argumentWrapperType && arg.type === rules.argumentWrapperType) {
186
+ arg = arg.namedChildren[0] || arg;
187
+ }
188
+ const unwrapped =
189
+ rules.spreadType && arg.type === rules.spreadType ? arg.namedChildren[0] || arg : arg;
190
+ if (!unwrapped) {
191
+ argIndex++;
192
+ continue;
193
+ }
205
194
 
206
- const argName = isIdent(unwrapped.type, rules) ? unwrapped.text : null;
207
- const argMember =
208
- rules.memberNode && unwrapped.type === rules.memberNode
209
- ? memberReceiver(unwrapped, rules)
210
- : null;
211
- const trackedName = argName || argMember;
212
-
213
- if (trackedName) {
214
- const binding = findBinding(trackedName);
215
- if (binding) {
216
- argFlows.push({
217
- callerFunc: scope.funcName,
218
- calleeName: callee,
219
- argIndex,
220
- argName: trackedName,
221
- binding,
222
- confidence: bindingConfidence(binding),
223
- expression: truncate(arg.text),
224
- line: node.startPosition.row + 1,
225
- });
226
- }
195
+ const argName = isIdent(unwrapped.type, rules) ? unwrapped.text : null;
196
+ const argMember =
197
+ rules.memberNode && unwrapped.type === rules.memberNode
198
+ ? memberReceiver(unwrapped, rules)
199
+ : null;
200
+ const trackedName = argName || argMember;
201
+
202
+ if (trackedName) {
203
+ const binding = findBinding(trackedName, scopeStack);
204
+ if (binding) {
205
+ argFlows.push({
206
+ callerFunc: scope.funcName,
207
+ calleeName: callee,
208
+ argIndex,
209
+ argName: trackedName,
210
+ binding,
211
+ confidence: bindingConfidence(binding),
212
+ expression: truncate(arg.text),
213
+ line: node.startPosition.row + 1,
214
+ });
227
215
  }
228
- argIndex++;
229
216
  }
217
+ argIndex++;
230
218
  }
219
+ }
231
220
 
232
- function handleExprStmtMutation(node) {
233
- if (rules.mutatingMethods.size === 0) return;
234
- const expr = node.namedChildren[0];
235
- if (!expr || !isCall(expr)) return;
221
+ function handleExprStmtMutation(node, rules, scopeStack, mutations, isCallNode) {
222
+ if (rules.mutatingMethods.size === 0) return;
223
+ const expr = node.namedChildren[0];
224
+ if (!expr || !isCall(expr, isCallNode)) return;
236
225
 
237
- let methodName = null;
238
- let receiver = null;
226
+ let methodName = null;
227
+ let receiver = null;
239
228
 
240
- const fn = expr.childForFieldName(rules.callFunctionField);
241
- if (fn && fn.type === rules.memberNode) {
242
- const prop = fn.childForFieldName(rules.memberPropertyField);
243
- methodName = prop ? prop.text : null;
244
- receiver = memberReceiver(fn, rules);
245
- }
229
+ const fn = expr.childForFieldName(rules.callFunctionField);
230
+ if (fn && fn.type === rules.memberNode) {
231
+ const prop = fn.childForFieldName(rules.memberPropertyField);
232
+ methodName = prop ? prop.text : null;
233
+ receiver = memberReceiver(fn, rules);
234
+ }
246
235
 
247
- if (!receiver && rules.callObjectField) {
248
- const obj = expr.childForFieldName(rules.callObjectField);
249
- const name = expr.childForFieldName(rules.callFunctionField);
250
- if (obj && name) {
251
- methodName = name.text;
252
- receiver = isIdent(obj.type, rules) ? obj.text : null;
253
- }
236
+ if (!receiver && rules.callObjectField) {
237
+ const obj = expr.childForFieldName(rules.callObjectField);
238
+ const name = expr.childForFieldName(rules.callFunctionField);
239
+ if (obj && name) {
240
+ methodName = name.text;
241
+ receiver = isIdent(obj.type, rules) ? obj.text : null;
254
242
  }
243
+ }
255
244
 
256
- if (!methodName || !rules.mutatingMethods.has(methodName)) return;
245
+ if (!methodName || !rules.mutatingMethods.has(methodName)) return;
257
246
 
258
- const scope = currentScope();
259
- if (!receiver || !scope?.funcName) return;
247
+ const scope = currentScope(scopeStack);
248
+ if (!receiver || !scope?.funcName) return;
260
249
 
261
- const binding = findBinding(receiver);
262
- if (binding) {
263
- mutations.push({
264
- funcName: scope.funcName,
265
- receiverName: receiver,
266
- binding,
267
- mutatingExpr: truncate(expr.text),
268
- line: node.startPosition.row + 1,
269
- });
270
- }
250
+ const binding = findBinding(receiver, scopeStack);
251
+ if (binding) {
252
+ mutations.push({
253
+ funcName: scope.funcName,
254
+ receiverName: receiver,
255
+ binding,
256
+ mutatingExpr: truncate(expr.text),
257
+ line: node.startPosition.row + 1,
258
+ });
271
259
  }
260
+ }
261
+
262
+ // ── Return statement handler ────────────────────────────────────────────
263
+
264
+ function handleReturn(node, rules, scopeStack, returns) {
265
+ if (node.parent?.type === rules.returnNode) return; // keyword token, not statement
266
+
267
+ const scope = currentScope(scopeStack);
268
+ if (scope?.funcName) {
269
+ const expr = node.namedChildren[0];
270
+ const referencedNames = [];
271
+ if (expr) collectIdentifiers(expr, referencedNames, rules);
272
+ returns.push({
273
+ funcName: scope.funcName,
274
+ expression: truncate(expr ? expr.text : ''),
275
+ referencedNames,
276
+ line: node.startPosition.row + 1,
277
+ });
278
+ }
279
+ }
280
+
281
+ // ── Visitor factory ─────────────────────────────────────────────────────
282
+
283
+ /**
284
+ * Create a dataflow visitor for use with walkWithVisitors.
285
+ *
286
+ * @param {object} rules - DATAFLOW_RULES for the language
287
+ * @returns {Visitor}
288
+ */
289
+ export function createDataflowVisitor(rules) {
290
+ const isCallNode = rules.callNodes ? (t) => rules.callNodes.has(t) : (t) => t === rules.callNode;
291
+
292
+ const parameters = [];
293
+ const returns = [];
294
+ const assignments = [];
295
+ const argFlows = [];
296
+ const mutations = [];
297
+ const scopeStack = [];
272
298
 
273
299
  return {
274
300
  name: 'dataflow',
@@ -300,54 +326,34 @@ export function createDataflowVisitor(rules) {
300
326
  enterNode(node, _context) {
301
327
  const t = node.type;
302
328
 
303
- // Skip function nodes — handled by enterFunction/exitFunction
304
329
  if (rules.functionNodes.has(t)) return;
305
330
 
306
- // Return statements (skip keyword tokens inside return statements, e.g. Ruby's
307
- // `return` node nests a `return` keyword child with the same type string)
308
331
  if (rules.returnNode && t === rules.returnNode) {
309
- if (node.parent?.type === rules.returnNode) return; // keyword token, not statement
310
-
311
- const scope = currentScope();
312
- if (scope?.funcName) {
313
- const expr = node.namedChildren[0];
314
- const referencedNames = [];
315
- if (expr) collectIdentifiers(expr, referencedNames, rules);
316
- returns.push({
317
- funcName: scope.funcName,
318
- expression: truncate(expr ? expr.text : ''),
319
- referencedNames,
320
- line: node.startPosition.row + 1,
321
- });
322
- }
332
+ handleReturn(node, rules, scopeStack, returns);
323
333
  return;
324
334
  }
325
335
 
326
- // Variable declarations
327
336
  if (rules.varDeclaratorNode && t === rules.varDeclaratorNode) {
328
- handleVarDeclarator(node);
337
+ handleVarDeclarator(node, rules, scopeStack, assignments, isCallNode);
329
338
  return;
330
339
  }
331
340
  if (rules.varDeclaratorNodes?.has(t)) {
332
- handleVarDeclarator(node);
341
+ handleVarDeclarator(node, rules, scopeStack, assignments, isCallNode);
333
342
  return;
334
343
  }
335
344
 
336
- // Call expressions
337
345
  if (isCallNode(t)) {
338
- handleCallExpr(node);
346
+ handleCallExpr(node, rules, scopeStack, argFlows);
339
347
  return;
340
348
  }
341
349
 
342
- // Assignment expressions
343
350
  if (rules.assignmentNode && t === rules.assignmentNode) {
344
- handleAssignment(node);
351
+ handleAssignment(node, rules, scopeStack, assignments, mutations, isCallNode);
345
352
  return;
346
353
  }
347
354
 
348
- // Mutation detection via expression_statement
349
355
  if (rules.expressionStmtNode && t === rules.expressionStmtNode) {
350
- handleExprStmtMutation(node);
356
+ handleExprStmtMutation(node, rules, scopeStack, mutations, isCallNode);
351
357
  }
352
358
  },
353
359
 
@@ -1,3 +1,4 @@
1
+ import { collectFile } from '../../db/query-builder.js';
1
2
  import { ConfigError } from '../../shared/errors.js';
2
3
 
3
4
  export const command = {
@@ -6,7 +7,7 @@ export const command = {
6
7
  queryOpts: true,
7
8
  options: [
8
9
  ['-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)'],
9
- ['-f, --file <path>', 'Scope to file (partial match)'],
10
+ ['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
10
11
  ],
11
12
  async execute([pattern], opts, ctx) {
12
13
  const { AST_NODE_KINDS, astQuery } = await import('../../ast.js');
@@ -1,3 +1,4 @@
1
+ import { collectFile } from '../../db/query-builder.js';
1
2
  import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
2
3
  import { audit } from '../../presentation/audit.js';
3
4
  import { explain } from '../../presentation/queries-cli.js';
@@ -10,7 +11,7 @@ export const command = {
10
11
  ['-d, --db <path>', 'Path to graph.db'],
11
12
  ['--quick', 'Structural summary only (skip impact analysis and health metrics)'],
12
13
  ['--depth <n>', 'Impact/explain depth', '3'],
13
- ['-f, --file <path>', 'Scope to file (partial match)'],
14
+ ['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
14
15
  ['-k, --kind <kind>', 'Filter by symbol kind'],
15
16
  ['-T, --no-tests', 'Exclude test/spec files from results'],
16
17
  ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import { collectFile } from '../../db/query-builder.js';
2
3
  import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
3
4
  import { BATCH_COMMANDS, multiBatchData, splitTargets } from '../../features/batch.js';
4
5
  import { batch } from '../../presentation/batch.js';
@@ -12,7 +13,7 @@ export const command = {
12
13
  ['--from-file <path>', 'Read targets from file (JSON array or newline-delimited)'],
13
14
  ['--stdin', 'Read targets from stdin (JSON array)'],
14
15
  ['--depth <n>', 'Traversal depth passed to underlying command'],
15
- ['-f, --file <path>', 'Scope to file (partial match)'],
16
+ ['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
16
17
  ['-k, --kind <kind>', 'Filter by symbol kind'],
17
18
  ['-T, --no-tests', 'Exclude test/spec files from results'],
18
19
  ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
@@ -0,0 +1,12 @@
1
+ import { brief } from '../../presentation/brief.js';
2
+
3
+ export const command = {
4
+ name: 'brief <file>',
5
+ description: 'Token-efficient file summary: symbols with roles, caller counts, risk tier',
6
+ queryOpts: true,
7
+ execute([file], opts, ctx) {
8
+ brief(file, opts.db, {
9
+ ...ctx.resolveQueryOpts(opts),
10
+ });
11
+ },
12
+ };
@@ -1,3 +1,4 @@
1
+ import { collectFile } from '../../db/query-builder.js';
1
2
  import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
2
3
 
3
4
  export const command = {
@@ -6,7 +7,7 @@ export const command = {
6
7
  queryOpts: true,
7
8
  options: [
8
9
  ['--format <fmt>', 'Output format: text, dot, mermaid', 'text'],
9
- ['-f, --file <path>', 'Scope to file (partial match)'],
10
+ ['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
10
11
  ['-k, --kind <kind>', 'Filter by symbol kind'],
11
12
  ],
12
13
  validate([_name], opts) {