@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
@@ -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,4 +1,5 @@
1
- import { ConfigError } from '../../errors.js';
1
+ import { collectFile } from '../../db/query-builder.js';
2
+ import { ConfigError } from '../../shared/errors.js';
2
3
 
3
4
  export const command = {
4
5
  name: 'ast [pattern]',
@@ -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');
@@ -16,11 +17,7 @@ export const command = {
16
17
  astQuery(pattern, opts.db, {
17
18
  kind: opts.kind,
18
19
  file: opts.file,
19
- noTests: ctx.resolveNoTests(opts),
20
- json: opts.json,
21
- ndjson: opts.ndjson,
22
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
23
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
20
+ ...ctx.resolveQueryOpts(opts),
24
21
  });
25
22
  },
26
23
  };
@@ -1,6 +1,8 @@
1
- import { audit } from '../../commands/audit.js';
2
- import { EVERY_SYMBOL_KIND } from '../../queries.js';
3
- import { explain } from '../../queries-cli.js';
1
+ import { collectFile } from '../../db/query-builder.js';
2
+ import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
3
+ import { audit } from '../../presentation/audit.js';
4
+ import { explain } from '../../presentation/queries-cli.js';
5
+ import { config } from '../shared/options.js';
4
6
 
5
7
  export const command = {
6
8
  name: 'audit <target>',
@@ -9,7 +11,7 @@ export const command = {
9
11
  ['-d, --db <path>', 'Path to graph.db'],
10
12
  ['--quick', 'Structural summary only (skip impact analysis and health metrics)'],
11
13
  ['--depth <n>', 'Impact/explain depth', '3'],
12
- ['-f, --file <path>', 'Scope to file (partial match)'],
14
+ ['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
13
15
  ['-k, --kind <kind>', 'Filter by symbol kind'],
14
16
  ['-T, --no-tests', 'Exclude test/spec files from results'],
15
17
  ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
@@ -24,14 +26,11 @@ export const command = {
24
26
  }
25
27
  },
26
28
  execute([target], opts, ctx) {
29
+ const qOpts = ctx.resolveQueryOpts(opts);
27
30
  if (opts.quick) {
28
31
  explain(target, opts.db, {
29
32
  depth: parseInt(opts.depth, 10),
30
- noTests: ctx.resolveNoTests(opts),
31
- json: opts.json,
32
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
33
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
34
- ndjson: opts.ndjson,
33
+ ...qOpts,
35
34
  });
36
35
  return;
37
36
  }
@@ -39,8 +38,9 @@ export const command = {
39
38
  depth: parseInt(opts.depth, 10),
40
39
  file: opts.file,
41
40
  kind: opts.kind,
42
- noTests: ctx.resolveNoTests(opts),
43
- json: opts.json,
41
+ noTests: qOpts.noTests,
42
+ json: qOpts.json,
43
+ config,
44
44
  });
45
45
  },
46
46
  };
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
- import { BATCH_COMMANDS, multiBatchData, splitTargets } from '../../batch.js';
3
- import { batch } from '../../commands/batch.js';
4
- import { ConfigError } from '../../errors.js';
5
- import { EVERY_SYMBOL_KIND } from '../../queries.js';
2
+ import { collectFile } from '../../db/query-builder.js';
3
+ import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
4
+ import { BATCH_COMMANDS, multiBatchData, splitTargets } from '../../features/batch.js';
5
+ import { batch } from '../../presentation/batch.js';
6
+ import { ConfigError } from '../../shared/errors.js';
6
7
 
7
8
  export const command = {
8
9
  name: 'batch <command> [targets...]',
@@ -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)'],
@@ -9,7 +9,7 @@ export const command = {
9
9
  ['-f, --format <format>', 'Output format: text, mermaid, json', 'text'],
10
10
  ],
11
11
  async execute([base, target], opts, ctx) {
12
- const { branchCompare } = await import('../../commands/branch-compare.js');
12
+ const { branchCompare } = await import('../../presentation/branch-compare.js');
13
13
  await branchCompare(base, target, {
14
14
  engine: ctx.program.opts().engine,
15
15
  depth: parseInt(opts.depth, 10),
@@ -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
+ };