@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
@@ -17,8 +17,8 @@
17
17
 
18
18
  import path from 'node:path';
19
19
  import { performance } from 'node:perf_hooks';
20
- import { bulkNodeIdsByFile } from '../db.js';
21
- import { debug } from '../logger.js';
20
+ import { bulkNodeIdsByFile } from '../db/index.js';
21
+ import { debug } from '../infrastructure/logger.js';
22
22
  import { computeLOCMetrics, computeMaintainabilityIndex } from './metrics.js';
23
23
  import {
24
24
  AST_TYPE_MAPS,
@@ -38,6 +38,7 @@ import { createDataflowVisitor } from './visitors/dataflow-visitor.js';
38
38
  // ─── Extension sets for quick language-support checks ────────────────────
39
39
 
40
40
  const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
41
+ const COMPLEXITY_EXTENSIONS = buildExtensionSet(COMPLEXITY_RULES);
41
42
  const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
42
43
  const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
43
44
 
@@ -45,282 +46,234 @@ const WALK_EXTENSIONS = buildExtensionSet(AST_TYPE_MAPS);
45
46
 
46
47
  let _parserModule = null;
47
48
  async function getParserModule() {
48
- if (!_parserModule) _parserModule = await import('../parser.js');
49
+ if (!_parserModule) _parserModule = await import('../domain/parser.js');
49
50
  return _parserModule;
50
51
  }
51
52
 
52
- // ─── Public API ──────────────────────────────────────────────────────────
53
+ // ─── WASM pre-parse ─────────────────────────────────────────────────────
53
54
 
54
- /**
55
- * Run all enabled AST analyses in a coordinated pass.
56
- *
57
- * @param {object} db - open better-sqlite3 database (read-write)
58
- * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, calls, _tree, _langId, ... }>
59
- * @param {string} rootDir - absolute project root path
60
- * @param {object} opts - build options (ast, complexity, cfg, dataflow toggles)
61
- * @param {object} [engineOpts] - engine options
62
- * @returns {Promise<{ astMs: number, complexityMs: number, cfgMs: number, dataflowMs: number }>}
63
- */
64
- export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
65
- const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
66
-
67
- const doAst = opts.ast !== false;
55
+ async function ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir) {
68
56
  const doComplexity = opts.complexity !== false;
69
57
  const doCfg = opts.cfg !== false;
70
58
  const doDataflow = opts.dataflow !== false;
71
59
 
72
- if (!doAst && !doComplexity && !doCfg && !doDataflow) return timing;
73
-
74
- const extToLang = buildExtToLangMap();
75
-
76
- // ── WASM pre-parse for files that need it ───────────────────────────
77
- // CFG now runs as a visitor in the unified walk, so only dataflow
78
- // triggers WASM pre-parse when no tree exists.
79
- if (doDataflow) {
80
- let needsWasmTrees = false;
81
- for (const [relPath, symbols] of fileSymbols) {
82
- if (symbols._tree) continue;
83
- const ext = path.extname(relPath).toLowerCase();
84
-
85
- if (!symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext)) {
86
- needsWasmTrees = true;
87
- break;
88
- }
89
- }
90
-
91
- if (needsWasmTrees) {
92
- try {
93
- const { ensureWasmTrees } = await getParserModule();
94
- await ensureWasmTrees(fileSymbols, rootDir);
95
- } catch (err) {
96
- debug(`ensureWasmTrees failed: ${err.message}`);
97
- }
98
- }
99
- }
100
-
101
- // ── Phase 7 Optimization: Unified pre-walk ─────────────────────────
102
- // For files with WASM trees, run all applicable visitors in a SINGLE
103
- // walkWithVisitors call. Store results in the format that buildXxx
104
- // functions already expect as pre-computed data (same fields as native
105
- // engine output). This eliminates ~3 redundant tree traversals per file.
106
- const t0walk = performance.now();
60
+ if (!doComplexity && !doCfg && !doDataflow) return;
107
61
 
62
+ let needsWasmTrees = false;
108
63
  for (const [relPath, symbols] of fileSymbols) {
109
- if (!symbols._tree) continue; // No WASM tree — native path handles it
110
-
64
+ if (symbols._tree) continue;
111
65
  const ext = path.extname(relPath).toLowerCase();
112
- const langId = symbols._langId || extToLang.get(ext);
113
- if (!langId) continue;
114
-
115
66
  const defs = symbols.definitions || [];
116
- const visitors = [];
117
- const walkerOpts = {
118
- functionNodeTypes: new Set(),
119
- nestingNodeTypes: new Set(),
120
- getFunctionName: (_node) => null,
121
- };
122
-
123
- // ─ AST-store visitor ─
124
- const astTypeMap = AST_TYPE_MAPS.get(langId);
125
- let astVisitor = null;
126
- if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !symbols.astNodes?.length) {
127
- const nodeIdMap = new Map();
128
- for (const row of bulkNodeIdsByFile(db, relPath)) {
129
- nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
130
- }
131
- astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
132
- visitors.push(astVisitor);
133
- }
134
67
 
135
- // Complexity visitor (file-level mode) ─
136
- const cRules = COMPLEXITY_RULES.get(langId);
137
- const hRules = HALSTEAD_RULES.get(langId);
138
- let complexityVisitor = null;
139
- if (doComplexity && cRules) {
140
- // Only use visitor if some functions lack pre-computed complexity
141
- const needsWasmComplexity = defs.some(
142
- (d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity,
143
- );
144
- if (needsWasmComplexity) {
145
- complexityVisitor = createComplexityVisitor(cRules, hRules, {
146
- fileLevelWalk: true,
147
- langId,
148
- });
149
- visitors.push(complexityVisitor);
150
-
151
- // Merge nesting nodes for complexity tracking
152
- // NOTE: do NOT add functionNodes here — funcDepth in the complexity
153
- // visitor already tracks function-level nesting. Adding them to
154
- // nestingNodeTypes would inflate context.nestingLevel by +1 inside
155
- // every function body, double-counting in cognitive += 1 + nestingLevel.
156
- for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes.add(t);
157
-
158
- // Provide getFunctionName for complexity visitor
159
- const dfRules = DATAFLOW_RULES.get(langId);
160
- walkerOpts.getFunctionName = (node) => {
161
- // Try complexity rules' function name field first
162
- const nameNode = node.childForFieldName('name');
163
- if (nameNode) return nameNode.text;
164
- // Fall back to dataflow rules' richer name extraction
165
- if (dfRules) return getFuncName(node, dfRules);
166
- return null;
167
- };
168
- }
169
- }
170
-
171
- // ─ CFG visitor ─
172
- const cfgRulesForLang = CFG_RULES.get(langId);
173
- let cfgVisitor = null;
174
- if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
175
- // Only use visitor if some functions lack pre-computed CFG
176
- const needsWasmCfg = defs.some(
68
+ const needsComplexity =
69
+ doComplexity &&
70
+ COMPLEXITY_EXTENSIONS.has(ext) &&
71
+ defs.some((d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity);
72
+ const needsCfg =
73
+ doCfg &&
74
+ CFG_EXTENSIONS.has(ext) &&
75
+ defs.some(
177
76
  (d) =>
178
77
  (d.kind === 'function' || d.kind === 'method') &&
179
78
  d.line &&
180
79
  d.cfg !== null &&
181
80
  !Array.isArray(d.cfg?.blocks),
182
81
  );
183
- if (needsWasmCfg) {
184
- cfgVisitor = createCfgVisitor(cfgRulesForLang);
185
- visitors.push(cfgVisitor);
186
- }
82
+ const needsDataflow = doDataflow && !symbols.dataflow && DATAFLOW_EXTENSIONS.has(ext);
83
+
84
+ if (needsComplexity || needsCfg || needsDataflow) {
85
+ needsWasmTrees = true;
86
+ break;
187
87
  }
88
+ }
188
89
 
189
- // Dataflow visitor ─
190
- const dfRules = DATAFLOW_RULES.get(langId);
191
- let dataflowVisitor = null;
192
- if (doDataflow && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
193
- dataflowVisitor = createDataflowVisitor(dfRules);
194
- visitors.push(dataflowVisitor);
90
+ if (needsWasmTrees) {
91
+ try {
92
+ const { ensureWasmTrees } = await getParserModule();
93
+ await ensureWasmTrees(fileSymbols, rootDir);
94
+ } catch (err) {
95
+ debug(`ensureWasmTrees failed: ${err.message}`);
195
96
  }
97
+ }
98
+ }
196
99
 
197
- // Run unified walk if we have visitors ─
198
- if (visitors.length === 0) continue;
100
+ // ─── Per-file visitor setup ─────────────────────────────────────────────
199
101
 
200
- const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
102
+ function setupVisitors(db, relPath, symbols, langId, opts) {
103
+ const ext = path.extname(relPath).toLowerCase();
104
+ const defs = symbols.definitions || [];
105
+ const doAst = opts.ast !== false;
106
+ const doComplexity = opts.complexity !== false;
107
+ const doCfg = opts.cfg !== false;
108
+ const doDataflow = opts.dataflow !== false;
201
109
 
202
- // Store AST results (buildAstNodes will find symbols.astNodes and skip its walk) ─
203
- if (astVisitor) {
204
- const astRows = results['ast-store'] || [];
205
- if (astRows.length > 0) {
206
- // Store in the format buildAstNodes expects for the native path
207
- symbols.astNodes = astRows;
208
- }
110
+ const visitors = [];
111
+ const walkerOpts = {
112
+ functionNodeTypes: new Set(),
113
+ nestingNodeTypes: new Set(),
114
+ getFunctionName: (_node) => null,
115
+ };
116
+
117
+ // AST-store visitor
118
+ let astVisitor = null;
119
+ const astTypeMap = AST_TYPE_MAPS.get(langId);
120
+ if (doAst && astTypeMap && WALK_EXTENSIONS.has(ext) && !symbols.astNodes?.length) {
121
+ const nodeIdMap = new Map();
122
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
123
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
209
124
  }
125
+ astVisitor = createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap);
126
+ visitors.push(astVisitor);
127
+ }
210
128
 
211
- // Store complexity results on definitions (buildComplexityMetrics will find def.complexity)
212
- if (complexityVisitor) {
213
- const complexityResults = results.complexity || [];
214
- // Match results back to definitions by function start line
215
- // Store the full result (metrics + funcNode) for O(1) lookup
216
- const resultByLine = new Map();
217
- for (const r of complexityResults) {
218
- if (r.funcNode) {
219
- const line = r.funcNode.startPosition.row + 1;
220
- if (!resultByLine.has(line)) resultByLine.set(line, []);
221
- resultByLine.get(line).push(r);
222
- }
223
- }
224
- for (const def of defs) {
225
- if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
226
- const candidates = resultByLine.get(def.line);
227
- const funcResult = !candidates
228
- ? undefined
229
- : candidates.length === 1
230
- ? candidates[0]
231
- : (candidates.find((r) => {
232
- const n = r.funcNode.childForFieldName('name');
233
- return n && n.text === def.name;
234
- }) ?? candidates[0]);
235
- if (funcResult) {
236
- const { metrics } = funcResult;
237
- const loc = computeLOCMetrics(funcResult.funcNode, langId);
238
- const volume = metrics.halstead ? metrics.halstead.volume : 0;
239
- const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
240
- const mi = computeMaintainabilityIndex(
241
- volume,
242
- metrics.cyclomatic,
243
- loc.sloc,
244
- commentRatio,
245
- );
246
-
247
- def.complexity = {
248
- cognitive: metrics.cognitive,
249
- cyclomatic: metrics.cyclomatic,
250
- maxNesting: metrics.maxNesting,
251
- halstead: metrics.halstead,
252
- loc,
253
- maintainabilityIndex: mi,
254
- };
255
- }
256
- }
257
- }
129
+ // Complexity visitor (file-level mode)
130
+ let complexityVisitor = null;
131
+ const cRules = COMPLEXITY_RULES.get(langId);
132
+ const hRules = HALSTEAD_RULES.get(langId);
133
+ if (doComplexity && cRules) {
134
+ const needsWasmComplexity = defs.some(
135
+ (d) => (d.kind === 'function' || d.kind === 'method') && d.line && !d.complexity,
136
+ );
137
+ if (needsWasmComplexity) {
138
+ complexityVisitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
139
+ visitors.push(complexityVisitor);
140
+
141
+ for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes.add(t);
142
+
143
+ const dfRules = DATAFLOW_RULES.get(langId);
144
+ walkerOpts.getFunctionName = (node) => {
145
+ const nameNode = node.childForFieldName('name');
146
+ if (nameNode) return nameNode.text;
147
+ if (dfRules) return getFuncName(node, dfRules);
148
+ return null;
149
+ };
258
150
  }
151
+ }
259
152
 
260
- // ─ Store CFG results on definitions (buildCFGData will find def.cfg and skip its walk) ─
261
- if (cfgVisitor) {
262
- const cfgResults = results.cfg || [];
263
- const cfgByLine = new Map();
264
- for (const r of cfgResults) {
265
- if (r.funcNode) {
266
- const line = r.funcNode.startPosition.row + 1;
267
- if (!cfgByLine.has(line)) cfgByLine.set(line, []);
268
- cfgByLine.get(line).push(r);
269
- }
270
- }
271
- for (const def of defs) {
272
- if (
273
- (def.kind === 'function' || def.kind === 'method') &&
274
- def.line &&
275
- !def.cfg?.blocks?.length
276
- ) {
277
- const candidates = cfgByLine.get(def.line);
278
- const cfgResult = !candidates
279
- ? undefined
280
- : candidates.length === 1
281
- ? candidates[0]
282
- : (candidates.find((r) => {
283
- const n = r.funcNode.childForFieldName('name');
284
- return n && n.text === def.name;
285
- }) ?? candidates[0]);
286
- if (cfgResult) {
287
- def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
288
-
289
- // Override complexity's cyclomatic with CFG-derived value (single source of truth)
290
- // and recompute maintainability index to stay consistent
291
- if (def.complexity && cfgResult.cyclomatic != null) {
292
- def.complexity.cyclomatic = cfgResult.cyclomatic;
293
- const { loc, halstead } = def.complexity;
294
- const volume = halstead ? halstead.volume : 0;
295
- const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
296
- def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
297
- volume,
298
- cfgResult.cyclomatic,
299
- loc?.sloc ?? 0,
300
- commentRatio,
301
- );
302
- }
303
- }
304
- }
305
- }
153
+ // CFG visitor
154
+ let cfgVisitor = null;
155
+ const cfgRulesForLang = CFG_RULES.get(langId);
156
+ if (doCfg && cfgRulesForLang && CFG_EXTENSIONS.has(ext)) {
157
+ const needsWasmCfg = defs.some(
158
+ (d) =>
159
+ (d.kind === 'function' || d.kind === 'method') &&
160
+ d.line &&
161
+ d.cfg !== null &&
162
+ !Array.isArray(d.cfg?.blocks),
163
+ );
164
+ if (needsWasmCfg) {
165
+ cfgVisitor = createCfgVisitor(cfgRulesForLang);
166
+ visitors.push(cfgVisitor);
306
167
  }
168
+ }
169
+
170
+ // Dataflow visitor
171
+ let dataflowVisitor = null;
172
+ const dfRules = DATAFLOW_RULES.get(langId);
173
+ if (doDataflow && dfRules && DATAFLOW_EXTENSIONS.has(ext) && !symbols.dataflow) {
174
+ dataflowVisitor = createDataflowVisitor(dfRules);
175
+ visitors.push(dataflowVisitor);
176
+ }
177
+
178
+ return { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor };
179
+ }
307
180
 
308
- // Store dataflow results (buildDataflowEdges will find symbols.dataflow and skip its walk) ─
309
- if (dataflowVisitor) {
310
- symbols.dataflow = results.dataflow;
181
+ // ─── Result storage helpers ─────────────────────────────────────────────
182
+
183
+ function storeComplexityResults(results, defs, langId) {
184
+ const complexityResults = results.complexity || [];
185
+ const resultByLine = new Map();
186
+ for (const r of complexityResults) {
187
+ if (r.funcNode) {
188
+ const line = r.funcNode.startPosition.row + 1;
189
+ if (!resultByLine.has(line)) resultByLine.set(line, []);
190
+ resultByLine.get(line).push(r);
311
191
  }
312
192
  }
193
+ for (const def of defs) {
194
+ if ((def.kind === 'function' || def.kind === 'method') && def.line && !def.complexity) {
195
+ const candidates = resultByLine.get(def.line);
196
+ const funcResult = !candidates
197
+ ? undefined
198
+ : candidates.length === 1
199
+ ? candidates[0]
200
+ : (candidates.find((r) => {
201
+ const n = r.funcNode.childForFieldName('name');
202
+ return n && n.text === def.name;
203
+ }) ?? candidates[0]);
204
+ if (funcResult) {
205
+ const { metrics } = funcResult;
206
+ const loc = computeLOCMetrics(funcResult.funcNode, langId);
207
+ const volume = metrics.halstead ? metrics.halstead.volume : 0;
208
+ const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
209
+ const mi = computeMaintainabilityIndex(volume, metrics.cyclomatic, loc.sloc, commentRatio);
210
+
211
+ def.complexity = {
212
+ cognitive: metrics.cognitive,
213
+ cyclomatic: metrics.cyclomatic,
214
+ maxNesting: metrics.maxNesting,
215
+ halstead: metrics.halstead,
216
+ loc,
217
+ maintainabilityIndex: mi,
218
+ };
219
+ }
220
+ }
221
+ }
222
+ }
313
223
 
314
- timing._unifiedWalkMs = performance.now() - t0walk;
224
+ function storeCfgResults(results, defs) {
225
+ const cfgResults = results.cfg || [];
226
+ const cfgByLine = new Map();
227
+ for (const r of cfgResults) {
228
+ if (r.funcNode) {
229
+ const line = r.funcNode.startPosition.row + 1;
230
+ if (!cfgByLine.has(line)) cfgByLine.set(line, []);
231
+ cfgByLine.get(line).push(r);
232
+ }
233
+ }
234
+ for (const def of defs) {
235
+ if (
236
+ (def.kind === 'function' || def.kind === 'method') &&
237
+ def.line &&
238
+ !def.cfg?.blocks?.length
239
+ ) {
240
+ const candidates = cfgByLine.get(def.line);
241
+ const cfgResult = !candidates
242
+ ? undefined
243
+ : candidates.length === 1
244
+ ? candidates[0]
245
+ : (candidates.find((r) => {
246
+ const n = r.funcNode.childForFieldName('name');
247
+ return n && n.text === def.name;
248
+ }) ?? candidates[0]);
249
+ if (cfgResult) {
250
+ def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
251
+
252
+ // Override complexity's cyclomatic with CFG-derived value (single source of truth)
253
+ if (def.complexity && cfgResult.cyclomatic != null) {
254
+ def.complexity.cyclomatic = cfgResult.cyclomatic;
255
+ const { loc, halstead } = def.complexity;
256
+ const volume = halstead ? halstead.volume : 0;
257
+ const commentRatio = loc?.loc > 0 ? loc.commentLines / loc.loc : 0;
258
+ def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
259
+ volume,
260
+ cfgResult.cyclomatic,
261
+ loc?.sloc ?? 0,
262
+ commentRatio,
263
+ );
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
315
269
 
316
- // ── Delegate to buildXxx functions ─────────────────────────────────
317
- // Each function finds pre-computed data from the unified walk above
318
- // (or from the native engine) and only does DB writes + native fallback.
270
+ // ─── Build delegation ───────────────────────────────────────────────────
319
271
 
320
- if (doAst) {
272
+ async function delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing) {
273
+ if (opts.ast !== false) {
321
274
  const t0 = performance.now();
322
275
  try {
323
- const { buildAstNodes } = await import('../ast.js');
276
+ const { buildAstNodes } = await import('../features/ast.js');
324
277
  await buildAstNodes(db, fileSymbols, rootDir, engineOpts);
325
278
  } catch (err) {
326
279
  debug(`buildAstNodes failed: ${err.message}`);
@@ -328,10 +281,10 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
328
281
  timing.astMs = performance.now() - t0;
329
282
  }
330
283
 
331
- if (doComplexity) {
284
+ if (opts.complexity !== false) {
332
285
  const t0 = performance.now();
333
286
  try {
334
- const { buildComplexityMetrics } = await import('../complexity.js');
287
+ const { buildComplexityMetrics } = await import('../features/complexity.js');
335
288
  await buildComplexityMetrics(db, fileSymbols, rootDir, engineOpts);
336
289
  } catch (err) {
337
290
  debug(`buildComplexityMetrics failed: ${err.message}`);
@@ -339,10 +292,10 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
339
292
  timing.complexityMs = performance.now() - t0;
340
293
  }
341
294
 
342
- if (doCfg) {
295
+ if (opts.cfg !== false) {
343
296
  const t0 = performance.now();
344
297
  try {
345
- const { buildCFGData } = await import('../cfg.js');
298
+ const { buildCFGData } = await import('../features/cfg.js');
346
299
  await buildCFGData(db, fileSymbols, rootDir, engineOpts);
347
300
  } catch (err) {
348
301
  debug(`buildCFGData failed: ${err.message}`);
@@ -350,16 +303,77 @@ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
350
303
  timing.cfgMs = performance.now() - t0;
351
304
  }
352
305
 
353
- if (doDataflow) {
306
+ if (opts.dataflow !== false) {
354
307
  const t0 = performance.now();
355
308
  try {
356
- const { buildDataflowEdges } = await import('../dataflow.js');
309
+ const { buildDataflowEdges } = await import('../features/dataflow.js');
357
310
  await buildDataflowEdges(db, fileSymbols, rootDir, engineOpts);
358
311
  } catch (err) {
359
312
  debug(`buildDataflowEdges failed: ${err.message}`);
360
313
  }
361
314
  timing.dataflowMs = performance.now() - t0;
362
315
  }
316
+ }
317
+
318
+ // ─── Public API ──────────────────────────────────────────────────────────
319
+
320
+ /**
321
+ * Run all enabled AST analyses in a coordinated pass.
322
+ *
323
+ * @param {object} db - open better-sqlite3 database (read-write)
324
+ * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, calls, _tree, _langId, ... }>
325
+ * @param {string} rootDir - absolute project root path
326
+ * @param {object} opts - build options (ast, complexity, cfg, dataflow toggles)
327
+ * @param {object} [engineOpts] - engine options
328
+ * @returns {Promise<{ astMs: number, complexityMs: number, cfgMs: number, dataflowMs: number }>}
329
+ */
330
+ export async function runAnalyses(db, fileSymbols, rootDir, opts, engineOpts) {
331
+ const timing = { astMs: 0, complexityMs: 0, cfgMs: 0, dataflowMs: 0 };
332
+
333
+ const doAst = opts.ast !== false;
334
+ const doComplexity = opts.complexity !== false;
335
+ const doCfg = opts.cfg !== false;
336
+ const doDataflow = opts.dataflow !== false;
337
+
338
+ if (!doAst && !doComplexity && !doCfg && !doDataflow) return timing;
339
+
340
+ const extToLang = buildExtToLangMap();
341
+
342
+ // WASM pre-parse for files that need it
343
+ await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
344
+
345
+ // Unified pre-walk: run all applicable visitors in a single DFS per file
346
+ const t0walk = performance.now();
347
+
348
+ for (const [relPath, symbols] of fileSymbols) {
349
+ if (!symbols._tree) continue;
350
+
351
+ const ext = path.extname(relPath).toLowerCase();
352
+ const langId = symbols._langId || extToLang.get(ext);
353
+ if (!langId) continue;
354
+
355
+ const { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor } =
356
+ setupVisitors(db, relPath, symbols, langId, opts);
357
+
358
+ if (visitors.length === 0) continue;
359
+
360
+ const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
361
+ const defs = symbols.definitions || [];
362
+
363
+ if (astVisitor) {
364
+ const astRows = results['ast-store'] || [];
365
+ if (astRows.length > 0) symbols.astNodes = astRows;
366
+ }
367
+
368
+ if (complexityVisitor) storeComplexityResults(results, defs, langId);
369
+ if (cfgVisitor) storeCfgResults(results, defs);
370
+ if (dataflowVisitor) symbols.dataflow = results.dataflow;
371
+ }
372
+
373
+ timing._unifiedWalkMs = performance.now() - t0walk;
374
+
375
+ // Delegate to buildXxx functions for DB writes + native fallback
376
+ await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
363
377
 
364
378
  return timing;
365
379
  }
@@ -2,8 +2,8 @@
2
2
  * Shared utilities for AST analysis modules (complexity, CFG, dataflow, AST nodes).
3
3
  */
4
4
 
5
- import { ConfigError } from '../errors.js';
6
- import { LANGUAGE_REGISTRY } from '../parser.js';
5
+ import { LANGUAGE_REGISTRY } from '../domain/parser.js';
6
+ import { ConfigError } from '../shared/errors.js';
7
7
 
8
8
  // ─── Generic Rule Factory ─────────────────────────────────────────────────
9
9
 
@@ -176,18 +176,6 @@ export function findFunctionNode(rootNode, startLine, _endLine, rules) {
176
176
  return best;
177
177
  }
178
178
 
179
- /**
180
- * Truncate a string to a maximum length, appending an ellipsis if truncated.
181
- *
182
- * @param {string} str - Input string
183
- * @param {number} [max=200] - Maximum length
184
- * @returns {string}
185
- */
186
- export function truncate(str, max = 200) {
187
- if (!str) return '';
188
- return str.length > max ? `${str.slice(0, max)}…` : str;
189
- }
190
-
191
179
  // ─── Extension / Language Mapping ─────────────────────────────────────────
192
180
 
193
181
  /**