@optave/codegraph 3.1.4 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +29 -72
  2. package/package.json +10 -8
  3. package/src/ast-analysis/engine.js +260 -246
  4. package/src/ast-analysis/shared.js +2 -14
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +4 -7
  9. package/src/cli/commands/audit.js +11 -11
  10. package/src/cli/commands/batch.js +6 -5
  11. package/src/cli/commands/branch-compare.js +1 -1
  12. package/src/cli/commands/brief.js +12 -0
  13. package/src/cli/commands/build.js +1 -1
  14. package/src/cli/commands/cfg.js +5 -8
  15. package/src/cli/commands/check.js +28 -36
  16. package/src/cli/commands/children.js +9 -7
  17. package/src/cli/commands/co-change.js +5 -3
  18. package/src/cli/commands/communities.js +2 -6
  19. package/src/cli/commands/complexity.js +5 -3
  20. package/src/cli/commands/context.js +9 -8
  21. package/src/cli/commands/cycles.js +12 -8
  22. package/src/cli/commands/dataflow.js +5 -8
  23. package/src/cli/commands/deps.js +9 -8
  24. package/src/cli/commands/diff-impact.js +2 -6
  25. package/src/cli/commands/embed.js +1 -1
  26. package/src/cli/commands/export.js +34 -31
  27. package/src/cli/commands/exports.js +2 -6
  28. package/src/cli/commands/flow.js +5 -8
  29. package/src/cli/commands/fn-impact.js +9 -8
  30. package/src/cli/commands/impact.js +2 -6
  31. package/src/cli/commands/info.js +2 -2
  32. package/src/cli/commands/map.js +1 -1
  33. package/src/cli/commands/mcp.js +1 -1
  34. package/src/cli/commands/models.js +1 -1
  35. package/src/cli/commands/owners.js +5 -3
  36. package/src/cli/commands/path.js +2 -2
  37. package/src/cli/commands/plot.js +40 -31
  38. package/src/cli/commands/query.js +9 -8
  39. package/src/cli/commands/registry.js +2 -2
  40. package/src/cli/commands/roles.js +5 -8
  41. package/src/cli/commands/search.js +9 -3
  42. package/src/cli/commands/sequence.js +5 -8
  43. package/src/cli/commands/snapshot.js +6 -1
  44. package/src/cli/commands/stats.js +1 -1
  45. package/src/cli/commands/structure.js +5 -4
  46. package/src/cli/commands/triage.js +41 -30
  47. package/src/cli/commands/watch.js +1 -1
  48. package/src/cli/commands/where.js +2 -6
  49. package/src/cli/index.js +11 -5
  50. package/src/cli/shared/open-graph.js +13 -0
  51. package/src/cli/shared/options.js +22 -2
  52. package/src/cli.js +1 -1
  53. package/src/db/connection.js +140 -11
  54. package/src/{db.js → db/index.js} +12 -5
  55. package/src/db/migrations.js +42 -65
  56. package/src/db/query-builder.js +72 -9
  57. package/src/db/repository/base.js +1 -1
  58. package/src/db/repository/graph-read.js +3 -3
  59. package/src/db/repository/in-memory-repository.js +30 -28
  60. package/src/db/repository/nodes.js +10 -17
  61. package/src/domain/analysis/brief.js +155 -0
  62. package/src/domain/analysis/context.js +392 -0
  63. package/src/domain/analysis/dependencies.js +395 -0
  64. package/src/{analysis → domain/analysis}/exports.js +11 -6
  65. package/src/domain/analysis/impact.js +581 -0
  66. package/src/domain/analysis/module-map.js +348 -0
  67. package/src/{analysis → domain/analysis}/roles.js +12 -9
  68. package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
  69. package/src/{builder → domain/graph/builder}/helpers.js +4 -4
  70. package/src/{builder → domain/graph/builder}/incremental.js +119 -93
  71. package/src/domain/graph/builder/pipeline.js +156 -0
  72. package/src/domain/graph/builder/stages/build-edges.js +376 -0
  73. package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
  74. package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
  75. package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
  76. package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
  77. package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
  78. package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
  79. package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
  80. package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/{cycles.js → domain/graph/cycles.js} +4 -4
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +2 -2
  85. package/src/{watcher.js → domain/graph/watcher.js} +7 -7
  86. package/src/{parser.js → domain/parser.js} +24 -15
  87. package/src/{queries.js → domain/queries.js} +17 -16
  88. package/src/{embeddings → domain/search}/generator.js +3 -3
  89. package/src/{embeddings → domain/search}/models.js +2 -2
  90. package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
  91. package/src/{embeddings → domain/search}/search/filters.js +9 -5
  92. package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
  93. package/src/{embeddings → domain/search}/search/keyword.js +13 -6
  94. package/src/{embeddings → domain/search}/search/prepare.js +15 -7
  95. package/src/{embeddings → domain/search}/search/semantic.js +1 -1
  96. package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
  97. package/src/extractors/csharp.js +224 -207
  98. package/src/extractors/go.js +176 -172
  99. package/src/extractors/hcl.js +94 -78
  100. package/src/extractors/java.js +213 -207
  101. package/src/extractors/javascript.js +275 -305
  102. package/src/extractors/php.js +234 -221
  103. package/src/extractors/python.js +252 -250
  104. package/src/extractors/ruby.js +192 -185
  105. package/src/extractors/rust.js +182 -167
  106. package/src/{ast.js → features/ast.js} +13 -11
  107. package/src/{audit.js → features/audit.js} +20 -46
  108. package/src/{batch.js → features/batch.js} +5 -5
  109. package/src/{boundaries.js → features/boundaries.js} +100 -85
  110. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  111. package/src/{cfg.js → features/cfg.js} +141 -150
  112. package/src/{check.js → features/check.js} +13 -30
  113. package/src/{cochange.js → features/cochange.js} +5 -5
  114. package/src/{communities.js → features/communities.js} +72 -57
  115. package/src/{complexity.js → features/complexity.js} +154 -143
  116. package/src/{dataflow.js → features/dataflow.js} +155 -158
  117. package/src/{export.js → features/export.js} +6 -6
  118. package/src/{flow.js → features/flow.js} +4 -4
  119. package/src/{viewer.js → features/graph-enrichment.js} +8 -8
  120. package/src/{manifesto.js → features/manifesto.js} +15 -12
  121. package/src/{owners.js → features/owners.js} +6 -5
  122. package/src/features/sequence.js +300 -0
  123. package/src/features/shared/find-nodes.js +31 -0
  124. package/src/{snapshot.js → features/snapshot.js} +3 -3
  125. package/src/{structure.js → features/structure.js} +139 -108
  126. package/src/features/triage.js +141 -0
  127. package/src/graph/builders/dependency.js +33 -14
  128. package/src/graph/classifiers/risk.js +3 -2
  129. package/src/graph/classifiers/roles.js +6 -3
  130. package/src/index.cjs +16 -0
  131. package/src/index.js +40 -39
  132. package/src/{native.js → infrastructure/native.js} +1 -1
  133. package/src/mcp/middleware.js +1 -1
  134. package/src/mcp/server.js +68 -59
  135. package/src/mcp/tool-registry.js +15 -2
  136. package/src/mcp/tools/ast-query.js +1 -1
  137. package/src/mcp/tools/audit.js +1 -1
  138. package/src/mcp/tools/batch-query.js +1 -1
  139. package/src/mcp/tools/branch-compare.js +3 -1
  140. package/src/mcp/tools/brief.js +8 -0
  141. package/src/mcp/tools/cfg.js +1 -1
  142. package/src/mcp/tools/check.js +3 -3
  143. package/src/mcp/tools/co-changes.js +1 -1
  144. package/src/mcp/tools/code-owners.js +1 -1
  145. package/src/mcp/tools/communities.js +1 -1
  146. package/src/mcp/tools/complexity.js +1 -1
  147. package/src/mcp/tools/dataflow.js +2 -2
  148. package/src/mcp/tools/execution-flow.js +2 -2
  149. package/src/mcp/tools/export-graph.js +2 -2
  150. package/src/mcp/tools/find-cycles.js +2 -2
  151. package/src/mcp/tools/index.js +2 -0
  152. package/src/mcp/tools/list-repos.js +1 -1
  153. package/src/mcp/tools/sequence.js +1 -1
  154. package/src/mcp/tools/structure.js +1 -1
  155. package/src/mcp/tools/triage.js +2 -2
  156. package/src/{commands → presentation}/audit.js +2 -2
  157. package/src/{commands → presentation}/batch.js +1 -1
  158. package/src/{commands → presentation}/branch-compare.js +2 -2
  159. package/src/presentation/brief.js +51 -0
  160. package/src/{commands → presentation}/cfg.js +1 -1
  161. package/src/{commands → presentation}/check.js +2 -2
  162. package/src/{commands → presentation}/communities.js +1 -1
  163. package/src/{commands → presentation}/complexity.js +1 -1
  164. package/src/{commands → presentation}/dataflow.js +1 -1
  165. package/src/{commands → presentation}/flow.js +2 -2
  166. package/src/{commands → presentation}/manifesto.js +1 -1
  167. package/src/{commands → presentation}/owners.js +1 -1
  168. package/src/presentation/queries-cli/exports.js +53 -0
  169. package/src/presentation/queries-cli/impact.js +214 -0
  170. package/src/presentation/queries-cli/index.js +5 -0
  171. package/src/presentation/queries-cli/inspect.js +329 -0
  172. package/src/presentation/queries-cli/overview.js +196 -0
  173. package/src/presentation/queries-cli/path.js +65 -0
  174. package/src/presentation/queries-cli.js +27 -0
  175. package/src/{commands → presentation}/query.js +1 -1
  176. package/src/presentation/result-formatter.js +126 -3
  177. package/src/{commands → presentation}/sequence.js +2 -2
  178. package/src/{commands → presentation}/structure.js +1 -1
  179. package/src/presentation/table.js +0 -8
  180. package/src/{commands → presentation}/triage.js +1 -1
  181. package/src/{constants.js → shared/constants.js} +1 -1
  182. package/src/shared/file-utils.js +2 -2
  183. package/src/shared/generators.js +9 -5
  184. package/src/shared/hierarchy.js +1 -1
  185. package/src/{kinds.js → shared/kinds.js} +1 -1
  186. package/src/analysis/context.js +0 -408
  187. package/src/analysis/dependencies.js +0 -341
  188. package/src/analysis/impact.js +0 -463
  189. package/src/analysis/module-map.js +0 -322
  190. package/src/builder/pipeline.js +0 -130
  191. package/src/builder/stages/build-edges.js +0 -297
  192. package/src/builder/stages/insert-nodes.js +0 -195
  193. package/src/mcp.js +0 -2
  194. package/src/queries-cli.js +0 -866
  195. package/src/sequence.js +0 -289
  196. package/src/triage.js +0 -126
  197. /package/src/{builder → domain/graph/builder}/context.js +0 -0
  198. /package/src/{builder.js → domain/graph/builder.js} +0 -0
  199. /package/src/{embeddings → domain/search}/index.js +0 -0
  200. /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
  201. /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
  202. /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
  203. /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
  204. /package/src/{config.js → infrastructure/config.js} +0 -0
  205. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  206. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  207. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  208. /package/src/{commands → presentation}/cochange.js +0 -0
  209. /package/src/{errors.js → shared/errors.js} +0 -0
  210. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -1,5 +1,5 @@
1
- import { isTestFile } from './infrastructure/test-filter.js';
2
- import { debug } from './logger.js';
1
+ import { debug } from '../infrastructure/logger.js';
2
+ import { isTestFile } from '../infrastructure/test-filter.js';
3
3
 
4
4
  // ─── Glob-to-Regex ───────────────────────────────────────────────────
5
5
 
@@ -94,104 +94,119 @@ export function resolveModules(boundaryConfig) {
94
94
  // ─── Validation ──────────────────────────────────────────────────────
95
95
 
96
96
  /**
97
- * Validate a boundary configuration object.
98
- * @param {object} config - The `manifesto.boundaries` config
99
- * @returns {{ valid: boolean, errors: string[] }}
97
+ * Validate the `modules` section of a boundary config.
98
+ * @param {object} modules
99
+ * @param {string[]} errors - Mutated: push any validation errors
100
100
  */
101
- export function validateBoundaryConfig(config) {
102
- const errors = [];
101
+ function validateModules(modules, errors) {
102
+ if (!modules || typeof modules !== 'object' || Object.keys(modules).length === 0) {
103
+ errors.push('boundaries.modules must be a non-empty object');
104
+ return;
105
+ }
106
+ for (const [name, value] of Object.entries(modules)) {
107
+ if (typeof value === 'string') continue;
108
+ if (value && typeof value === 'object' && typeof value.match === 'string') continue;
109
+ errors.push(`boundaries.modules.${name}: must be a glob string or { match: "<glob>" }`);
110
+ }
111
+ }
103
112
 
104
- if (!config || typeof config !== 'object') {
105
- return { valid: false, errors: ['boundaries config must be an object'] };
113
+ /**
114
+ * Validate the `preset` field of a boundary config.
115
+ * @param {string|null|undefined} preset
116
+ * @param {string[]} errors - Mutated: push any validation errors
117
+ */
118
+ function validatePreset(preset, errors) {
119
+ if (preset == null) return;
120
+ if (typeof preset !== 'string' || !PRESETS[preset]) {
121
+ errors.push(
122
+ `boundaries.preset: must be one of ${Object.keys(PRESETS).join(', ')} (got "${preset}")`,
123
+ );
106
124
  }
125
+ }
107
126
 
108
- // Validate modules
109
- if (
110
- !config.modules ||
111
- typeof config.modules !== 'object' ||
112
- Object.keys(config.modules).length === 0
113
- ) {
114
- errors.push('boundaries.modules must be a non-empty object');
115
- } else {
116
- for (const [name, value] of Object.entries(config.modules)) {
117
- if (typeof value === 'string') continue;
118
- if (value && typeof value === 'object' && typeof value.match === 'string') continue;
119
- errors.push(`boundaries.modules.${name}: must be a glob string or { match: "<glob>" }`);
127
+ /**
128
+ * Validate a single rule's target list (`notTo` or `onlyTo`).
129
+ * @param {*} list - The target list value
130
+ * @param {string} field - "notTo" or "onlyTo"
131
+ * @param {number} idx - Rule index for error messages
132
+ * @param {Set<string>} moduleNames
133
+ * @param {string[]} errors - Mutated
134
+ */
135
+ function validateTargetList(list, field, idx, moduleNames, errors) {
136
+ if (!Array.isArray(list)) {
137
+ errors.push(`boundaries.rules[${idx}]: "${field}" must be an array`);
138
+ return;
139
+ }
140
+ for (const target of list) {
141
+ if (!moduleNames.has(target)) {
142
+ errors.push(`boundaries.rules[${idx}]: "${field}" references unknown module "${target}"`);
120
143
  }
121
144
  }
145
+ }
122
146
 
123
- // Validate preset
124
- if (config.preset != null) {
125
- if (typeof config.preset !== 'string' || !PRESETS[config.preset]) {
126
- errors.push(
127
- `boundaries.preset: must be one of ${Object.keys(PRESETS).join(', ')} (got "${config.preset}")`,
128
- );
147
+ /**
148
+ * Validate the `rules` array of a boundary config.
149
+ * @param {Array} rules
150
+ * @param {object|undefined} modules - The modules config (for cross-referencing names)
151
+ * @param {string[]} errors - Mutated
152
+ */
153
+ function validateRules(rules, modules, errors) {
154
+ if (!rules) return;
155
+ if (!Array.isArray(rules)) {
156
+ errors.push('boundaries.rules must be an array');
157
+ return;
158
+ }
159
+ const moduleNames = modules ? new Set(Object.keys(modules)) : new Set();
160
+ for (let i = 0; i < rules.length; i++) {
161
+ const rule = rules[i];
162
+ if (!rule.from) {
163
+ errors.push(`boundaries.rules[${i}]: missing "from" field`);
164
+ } else if (!moduleNames.has(rule.from)) {
165
+ errors.push(`boundaries.rules[${i}]: "from" references unknown module "${rule.from}"`);
166
+ }
167
+ if (rule.notTo && rule.onlyTo) {
168
+ errors.push(`boundaries.rules[${i}]: cannot have both "notTo" and "onlyTo"`);
169
+ }
170
+ if (!rule.notTo && !rule.onlyTo) {
171
+ errors.push(`boundaries.rules[${i}]: must have either "notTo" or "onlyTo"`);
129
172
  }
173
+ if (rule.notTo) validateTargetList(rule.notTo, 'notTo', i, moduleNames, errors);
174
+ if (rule.onlyTo) validateTargetList(rule.onlyTo, 'onlyTo', i, moduleNames, errors);
130
175
  }
176
+ }
131
177
 
132
- // Validate rules
133
- if (config.rules) {
134
- if (!Array.isArray(config.rules)) {
135
- errors.push('boundaries.rules must be an array');
136
- } else {
137
- const moduleNames = config.modules ? new Set(Object.keys(config.modules)) : new Set();
138
- for (let i = 0; i < config.rules.length; i++) {
139
- const rule = config.rules[i];
140
- if (!rule.from) {
141
- errors.push(`boundaries.rules[${i}]: missing "from" field`);
142
- } else if (!moduleNames.has(rule.from)) {
143
- errors.push(`boundaries.rules[${i}]: "from" references unknown module "${rule.from}"`);
144
- }
145
- if (rule.notTo && rule.onlyTo) {
146
- errors.push(`boundaries.rules[${i}]: cannot have both "notTo" and "onlyTo"`);
147
- }
148
- if (!rule.notTo && !rule.onlyTo) {
149
- errors.push(`boundaries.rules[${i}]: must have either "notTo" or "onlyTo"`);
150
- }
151
- if (rule.notTo) {
152
- if (!Array.isArray(rule.notTo)) {
153
- errors.push(`boundaries.rules[${i}]: "notTo" must be an array`);
154
- } else {
155
- for (const target of rule.notTo) {
156
- if (!moduleNames.has(target)) {
157
- errors.push(
158
- `boundaries.rules[${i}]: "notTo" references unknown module "${target}"`,
159
- );
160
- }
161
- }
162
- }
163
- }
164
- if (rule.onlyTo) {
165
- if (!Array.isArray(rule.onlyTo)) {
166
- errors.push(`boundaries.rules[${i}]: "onlyTo" must be an array`);
167
- } else {
168
- for (const target of rule.onlyTo) {
169
- if (!moduleNames.has(target)) {
170
- errors.push(
171
- `boundaries.rules[${i}]: "onlyTo" references unknown module "${target}"`,
172
- );
173
- }
174
- }
175
- }
176
- }
177
- }
178
+ /**
179
+ * Validate that module layer assignments match preset layers.
180
+ * @param {object} config
181
+ * @param {string[]} errors - Mutated
182
+ */
183
+ function validateLayerAssignments(config, errors) {
184
+ if (!config.preset || !PRESETS[config.preset] || !config.modules) return;
185
+ const presetLayers = new Set(PRESETS[config.preset].layers);
186
+ for (const [name, value] of Object.entries(config.modules)) {
187
+ if (typeof value === 'object' && value.layer && !presetLayers.has(value.layer)) {
188
+ errors.push(
189
+ `boundaries.modules.${name}: layer "${value.layer}" not in preset "${config.preset}" (valid: ${[...presetLayers].join(', ')})`,
190
+ );
178
191
  }
179
192
  }
193
+ }
180
194
 
181
- // Validate preset + layer assignments
182
- if (config.preset && PRESETS[config.preset] && config.modules) {
183
- const presetLayers = new Set(PRESETS[config.preset].layers);
184
- for (const [name, value] of Object.entries(config.modules)) {
185
- if (typeof value === 'object' && value.layer) {
186
- if (!presetLayers.has(value.layer)) {
187
- errors.push(
188
- `boundaries.modules.${name}: layer "${value.layer}" not in preset "${config.preset}" (valid: ${[...presetLayers].join(', ')})`,
189
- );
190
- }
191
- }
192
- }
195
+ /**
196
+ * Validate a boundary configuration object.
197
+ * @param {object} config - The `manifesto.boundaries` config
198
+ * @returns {{ valid: boolean, errors: string[] }}
199
+ */
200
+ export function validateBoundaryConfig(config) {
201
+ if (!config || typeof config !== 'object') {
202
+ return { valid: false, errors: ['boundaries config must be an object'] };
193
203
  }
194
204
 
205
+ const errors = [];
206
+ validateModules(config.modules, errors);
207
+ validatePreset(config.preset, errors);
208
+ validateRules(config.rules, config.modules, errors);
209
+ validateLayerAssignments(config, errors);
195
210
  return { valid: errors.length === 0, errors };
196
211
  }
197
212
 
@@ -11,9 +11,9 @@ import fs from 'node:fs';
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import Database from 'better-sqlite3';
14
- import { buildGraph } from './builder.js';
15
- import { isTestFile } from './infrastructure/test-filter.js';
16
- import { kindIcon } from './queries.js';
14
+ import { buildGraph } from '../domain/graph/builder.js';
15
+ import { kindIcon } from '../domain/queries.js';
16
+ import { isTestFile } from '../infrastructure/test-filter.js';
17
17
 
18
18
  // ─── Git Helpers ────────────────────────────────────────────────────────
19
19
 
@@ -7,14 +7,14 @@
7
7
 
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
- import { CFG_RULES } from './ast-analysis/rules/index.js';
10
+ import { CFG_RULES } from '../ast-analysis/rules/index.js';
11
11
  import {
12
12
  makeCfgRules as _makeCfgRules,
13
13
  buildExtensionSet,
14
14
  buildExtToLangMap,
15
- } from './ast-analysis/shared.js';
16
- import { walkWithVisitors } from './ast-analysis/visitor.js';
17
- import { createCfgVisitor } from './ast-analysis/visitors/cfg-visitor.js';
15
+ } from '../ast-analysis/shared.js';
16
+ import { walkWithVisitors } from '../ast-analysis/visitor.js';
17
+ import { createCfgVisitor } from '../ast-analysis/visitors/cfg-visitor.js';
18
18
  import {
19
19
  deleteCfgForNode,
20
20
  getCfgBlocks,
@@ -22,10 +22,10 @@ import {
22
22
  getFunctionNodeId,
23
23
  hasCfgTables,
24
24
  openReadonlyOrFail,
25
- } from './db.js';
26
- import { isTestFile } from './infrastructure/test-filter.js';
27
- import { info } from './logger.js';
28
- import { paginateResult } from './paginate.js';
25
+ } from '../db/index.js';
26
+ import { debug, info } from '../infrastructure/logger.js';
27
+ import { paginateResult } from '../shared/paginate.js';
28
+ import { findNodes } from './shared/find-nodes.js';
29
29
 
30
30
  // Re-export for backward compatibility
31
31
  export { _makeCfgRules as makeCfgRules, CFG_RULES };
@@ -68,30 +68,15 @@ export function buildFunctionCFG(functionNode, langId) {
68
68
  return { blocks: r.blocks, edges: r.edges, cyclomatic: r.cyclomatic };
69
69
  }
70
70
 
71
- // ─── Build-Time: Compute CFG for Changed Files ─────────────────────────
71
+ // ─── Build-Time Helpers ─────────────────────────────────────────────────
72
72
 
73
- /**
74
- * Build CFG data for all function/method definitions and persist to DB.
75
- *
76
- * @param {object} db - open better-sqlite3 database (read-write)
77
- * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, _tree, _langId }>
78
- * @param {string} rootDir - absolute project root path
79
- * @param {object} [_engineOpts] - engine options (unused; always uses WASM for AST)
80
- */
81
- export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
82
- // Lazily init WASM parsers if needed
83
- let parsers = null;
73
+ async function initCfgParsers(fileSymbols) {
84
74
  let needsFallback = false;
85
75
 
86
- // Always build ext→langId map so native-only builds (where _langId is unset)
87
- // can still derive the language from the file extension.
88
- const extToLang = buildExtToLangMap();
89
-
90
76
  for (const [relPath, symbols] of fileSymbols) {
91
77
  if (!symbols._tree) {
92
78
  const ext = path.extname(relPath).toLowerCase();
93
79
  if (CFG_EXTENSIONS.has(ext)) {
94
- // Check if all function/method defs already have native CFG data
95
80
  const hasNativeCfg = symbols.definitions
96
81
  .filter((d) => (d.kind === 'function' || d.kind === 'method') && d.line)
97
82
  .every((d) => d.cfg === null || d.cfg?.blocks?.length);
@@ -103,18 +88,131 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
103
88
  }
104
89
  }
105
90
 
91
+ let parsers = null;
92
+ let getParserFn = null;
93
+
106
94
  if (needsFallback) {
107
- const { createParsers } = await import('./parser.js');
95
+ const { createParsers } = await import('../domain/parser.js');
108
96
  parsers = await createParsers();
97
+ const mod = await import('../domain/parser.js');
98
+ getParserFn = mod.getParser;
109
99
  }
110
100
 
111
- let getParserFn = null;
112
- if (parsers) {
113
- const mod = await import('./parser.js');
114
- getParserFn = mod.getParser;
101
+ return { parsers, getParserFn };
102
+ }
103
+
104
+ function getTreeAndLang(symbols, relPath, rootDir, extToLang, parsers, getParserFn) {
105
+ const ext = path.extname(relPath).toLowerCase();
106
+ let tree = symbols._tree;
107
+ let langId = symbols._langId;
108
+
109
+ const allNative = symbols.definitions
110
+ .filter((d) => (d.kind === 'function' || d.kind === 'method') && d.line)
111
+ .every((d) => d.cfg === null || d.cfg?.blocks?.length);
112
+
113
+ if (!tree && !allNative) {
114
+ if (!getParserFn) return null;
115
+ langId = extToLang.get(ext);
116
+ if (!langId || !CFG_RULES.has(langId)) return null;
117
+
118
+ const absPath = path.join(rootDir, relPath);
119
+ let code;
120
+ try {
121
+ code = fs.readFileSync(absPath, 'utf-8');
122
+ } catch (e) {
123
+ debug(`cfg: cannot read ${relPath}: ${e.message}`);
124
+ return null;
125
+ }
126
+
127
+ const parser = getParserFn(parsers, absPath);
128
+ if (!parser) return null;
129
+
130
+ try {
131
+ tree = parser.parse(code);
132
+ } catch (e) {
133
+ debug(`cfg: parse failed for ${relPath}: ${e.message}`);
134
+ return null;
135
+ }
136
+ }
137
+
138
+ if (!langId) {
139
+ langId = extToLang.get(ext);
140
+ if (!langId) return null;
141
+ }
142
+
143
+ return { tree, langId };
144
+ }
145
+
146
+ function buildVisitorCfgMap(tree, cfgRules, symbols, langId) {
147
+ const needsVisitor =
148
+ tree &&
149
+ symbols.definitions.some(
150
+ (d) =>
151
+ (d.kind === 'function' || d.kind === 'method') &&
152
+ d.line &&
153
+ d.cfg !== null &&
154
+ !d.cfg?.blocks?.length,
155
+ );
156
+ if (!needsVisitor) return null;
157
+
158
+ const visitor = createCfgVisitor(cfgRules);
159
+ const walkerOpts = {
160
+ functionNodeTypes: new Set(cfgRules.functionNodes),
161
+ nestingNodeTypes: new Set(),
162
+ getFunctionName: (node) => {
163
+ const nameNode = node.childForFieldName('name');
164
+ return nameNode ? nameNode.text : null;
165
+ },
166
+ };
167
+ const walkResults = walkWithVisitors(tree.rootNode, [visitor], langId, walkerOpts);
168
+ const cfgResults = walkResults.cfg || [];
169
+ const visitorCfgByLine = new Map();
170
+ for (const r of cfgResults) {
171
+ if (r.funcNode) {
172
+ const line = r.funcNode.startPosition.row + 1;
173
+ if (!visitorCfgByLine.has(line)) visitorCfgByLine.set(line, []);
174
+ visitorCfgByLine.get(line).push(r);
175
+ }
115
176
  }
177
+ return visitorCfgByLine;
178
+ }
179
+
180
+ function persistCfg(cfg, nodeId, insertBlock, insertEdge) {
181
+ const blockDbIds = new Map();
182
+ for (const block of cfg.blocks) {
183
+ const result = insertBlock.run(
184
+ nodeId,
185
+ block.index,
186
+ block.type,
187
+ block.startLine,
188
+ block.endLine,
189
+ block.label,
190
+ );
191
+ blockDbIds.set(block.index, result.lastInsertRowid);
192
+ }
193
+
194
+ for (const edge of cfg.edges) {
195
+ const sourceDbId = blockDbIds.get(edge.sourceIndex);
196
+ const targetDbId = blockDbIds.get(edge.targetIndex);
197
+ if (sourceDbId && targetDbId) {
198
+ insertEdge.run(nodeId, sourceDbId, targetDbId, edge.kind);
199
+ }
200
+ }
201
+ }
116
202
 
117
- // findFunctionNode imported from ./ast-analysis/shared.js at module level
203
+ // ─── Build-Time: Compute CFG for Changed Files ─────────────────────────
204
+
205
+ /**
206
+ * Build CFG data for all function/method definitions and persist to DB.
207
+ *
208
+ * @param {object} db - open better-sqlite3 database (read-write)
209
+ * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, _tree, _langId }>
210
+ * @param {string} rootDir - absolute project root path
211
+ * @param {object} [_engineOpts] - engine options (unused; always uses WASM for AST)
212
+ */
213
+ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
214
+ const extToLang = buildExtToLangMap();
215
+ const { parsers, getParserFn } = await initCfgParsers(fileSymbols);
118
216
 
119
217
  const insertBlock = db.prepare(
120
218
  `INSERT INTO cfg_blocks (function_node_id, block_index, block_type, start_line, end_line, label)
@@ -131,79 +229,14 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
131
229
  const ext = path.extname(relPath).toLowerCase();
132
230
  if (!CFG_EXTENSIONS.has(ext)) continue;
133
231
 
134
- let tree = symbols._tree;
135
- let langId = symbols._langId;
136
-
137
- // Check if all defs already have native CFG — skip WASM parse if so
138
- const allNative = symbols.definitions
139
- .filter((d) => (d.kind === 'function' || d.kind === 'method') && d.line)
140
- .every((d) => d.cfg === null || d.cfg?.blocks?.length);
141
-
142
- // WASM fallback if no cached tree and not all native
143
- if (!tree && !allNative) {
144
- if (!getParserFn) continue;
145
- langId = extToLang.get(ext);
146
- if (!langId || !CFG_RULES.has(langId)) continue;
147
-
148
- const absPath = path.join(rootDir, relPath);
149
- let code;
150
- try {
151
- code = fs.readFileSync(absPath, 'utf-8');
152
- } catch {
153
- continue;
154
- }
155
-
156
- const parser = getParserFn(parsers, absPath);
157
- if (!parser) continue;
158
-
159
- try {
160
- tree = parser.parse(code);
161
- } catch {
162
- continue;
163
- }
164
- }
165
-
166
- if (!langId) {
167
- langId = extToLang.get(ext);
168
- if (!langId) continue;
169
- }
232
+ const treeLang = getTreeAndLang(symbols, relPath, rootDir, extToLang, parsers, getParserFn);
233
+ if (!treeLang) continue;
234
+ const { tree, langId } = treeLang;
170
235
 
171
236
  const cfgRules = CFG_RULES.get(langId);
172
237
  if (!cfgRules) continue;
173
238
 
174
- // WASM fallback: run file-level visitor walk to compute CFG for all functions
175
- // that don't already have pre-computed data (from native engine or unified walk)
176
- let visitorCfgByLine = null;
177
- const needsVisitor =
178
- tree &&
179
- symbols.definitions.some(
180
- (d) =>
181
- (d.kind === 'function' || d.kind === 'method') &&
182
- d.line &&
183
- d.cfg !== null &&
184
- !d.cfg?.blocks?.length,
185
- );
186
- if (needsVisitor) {
187
- const visitor = createCfgVisitor(cfgRules);
188
- const walkerOpts = {
189
- functionNodeTypes: new Set(cfgRules.functionNodes),
190
- nestingNodeTypes: new Set(),
191
- getFunctionName: (node) => {
192
- const nameNode = node.childForFieldName('name');
193
- return nameNode ? nameNode.text : null;
194
- },
195
- };
196
- const walkResults = walkWithVisitors(tree.rootNode, [visitor], langId, walkerOpts);
197
- const cfgResults = walkResults.cfg || [];
198
- visitorCfgByLine = new Map();
199
- for (const r of cfgResults) {
200
- if (r.funcNode) {
201
- const line = r.funcNode.startPosition.row + 1;
202
- if (!visitorCfgByLine.has(line)) visitorCfgByLine.set(line, []);
203
- visitorCfgByLine.get(line).push(r);
204
- }
205
- }
206
- }
239
+ const visitorCfgByLine = buildVisitorCfgMap(tree, cfgRules, symbols, langId);
207
240
 
208
241
  for (const def of symbols.definitions) {
209
242
  if (def.kind !== 'function' && def.kind !== 'method') continue;
@@ -212,7 +245,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
212
245
  const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
213
246
  if (!nodeId) continue;
214
247
 
215
- // Use pre-computed CFG (native engine or unified walk), then visitor fallback
216
248
  let cfg = null;
217
249
  if (def.cfg?.blocks?.length) {
218
250
  cfg = def.cfg;
@@ -231,36 +263,10 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
231
263
 
232
264
  if (!cfg || cfg.blocks.length === 0) continue;
233
265
 
234
- // Clear old CFG data for this function
235
266
  deleteCfgForNode(db, nodeId);
236
-
237
- // Insert blocks and build index→dbId mapping
238
- const blockDbIds = new Map();
239
- for (const block of cfg.blocks) {
240
- const result = insertBlock.run(
241
- nodeId,
242
- block.index,
243
- block.type,
244
- block.startLine,
245
- block.endLine,
246
- block.label,
247
- );
248
- blockDbIds.set(block.index, result.lastInsertRowid);
249
- }
250
-
251
- // Insert edges
252
- for (const edge of cfg.edges) {
253
- const sourceDbId = blockDbIds.get(edge.sourceIndex);
254
- const targetDbId = blockDbIds.get(edge.targetIndex);
255
- if (sourceDbId && targetDbId) {
256
- insertEdge.run(nodeId, sourceDbId, targetDbId, edge.kind);
257
- }
258
- }
259
-
267
+ persistCfg(cfg, nodeId, insertBlock, insertEdge);
260
268
  analyzed++;
261
269
  }
262
-
263
- // Don't release _tree here — complexity/dataflow may still need it
264
270
  }
265
271
  });
266
272
 
@@ -273,27 +279,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
273
279
 
274
280
  // ─── Query-Time Functions ───────────────────────────────────────────────
275
281
 
276
- function findNodes(db, name, opts = {}) {
277
- const kinds = opts.kind ? [opts.kind] : ['function', 'method'];
278
- const placeholders = kinds.map(() => '?').join(', ');
279
- const params = [`%${name}%`, ...kinds];
280
-
281
- let fileCondition = '';
282
- if (opts.file) {
283
- fileCondition = ' AND n.file LIKE ?';
284
- params.push(`%${opts.file}%`);
285
- }
286
-
287
- const rows = db
288
- .prepare(
289
- `SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
290
- FROM nodes n
291
- WHERE n.name LIKE ? AND n.kind IN (${placeholders})${fileCondition}`,
292
- )
293
- .all(...params);
294
-
295
- return opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
296
- }
282
+ const CFG_DEFAULT_KINDS = ['function', 'method'];
297
283
 
298
284
  /**
299
285
  * Load CFG data for a function from the database.
@@ -317,7 +303,12 @@ export function cfgData(name, customDbPath, opts = {}) {
317
303
  };
318
304
  }
319
305
 
320
- const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
306
+ const nodes = findNodes(
307
+ db,
308
+ name,
309
+ { noTests, file: opts.file, kind: opts.kind },
310
+ CFG_DEFAULT_KINDS,
311
+ );
321
312
  if (nodes.length === 0) {
322
313
  return { name, results: [] };
323
314
  }
@@ -1,10 +1,11 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { loadConfig } from './config.js';
5
- import { findCycles } from './cycles.js';
6
- import { findDbPath, openReadonlyOrFail } from './db.js';
7
- import { isTestFile } from './infrastructure/test-filter.js';
4
+ import { findDbPath, openReadonlyOrFail } from '../db/index.js';
5
+ import { bfsTransitiveCallers } from '../domain/analysis/impact.js';
6
+ import { findCycles } from '../domain/graph/cycles.js';
7
+ import { loadConfig } from '../infrastructure/config.js';
8
+ import { isTestFile } from '../infrastructure/test-filter.js';
8
9
  import { matchOwners, parseCodeowners } from './owners.js';
9
10
 
10
11
  // ─── Diff Parser ──────────────────────────────────────────────────────
@@ -96,31 +97,10 @@ export function checkMaxBlastRadius(db, changedRanges, threshold, noTests, maxDe
96
97
  }
97
98
  if (!overlaps) continue;
98
99
 
99
- // BFS transitive callers
100
- const visited = new Set([def.id]);
101
- let frontier = [def.id];
102
- let totalCallers = 0;
103
- for (let d = 1; d <= maxDepth; d++) {
104
- const nextFrontier = [];
105
- for (const fid of frontier) {
106
- const callers = db
107
- .prepare(
108
- `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
109
- FROM edges e JOIN nodes n ON e.source_id = n.id
110
- WHERE e.target_id = ? AND e.kind = 'calls'`,
111
- )
112
- .all(fid);
113
- for (const c of callers) {
114
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
115
- visited.add(c.id);
116
- nextFrontier.push(c.id);
117
- totalCallers++;
118
- }
119
- }
120
- }
121
- frontier = nextFrontier;
122
- if (frontier.length === 0) break;
123
- }
100
+ const { totalDependents: totalCallers } = bfsTransitiveCallers(db, def.id, {
101
+ noTests,
102
+ maxDepth,
103
+ });
124
104
 
125
105
  if (totalCallers > maxFound) maxFound = totalCallers;
126
106
  if (totalCallers > threshold) {
@@ -240,7 +220,10 @@ export function checkData(customDbPath, opts = {}) {
240
220
  const maxDepth = opts.depth || 3;
241
221
 
242
222
  // Load config defaults for check predicates
243
- const config = loadConfig(repoRoot);
223
+ // NOTE: opts.config is loaded from process.cwd() at startup (via CLI context),
224
+ // which may differ from the DB's parent repo root when --db points to an external
225
+ // project. This is an acceptable trade-off to avoid duplicate I/O on the hot path.
226
+ const config = opts.config || loadConfig(repoRoot);
244
227
  const checkConfig = config.check || {};
245
228
 
246
229
  // Resolve which predicates are enabled: CLI flags ?? config ?? built-in defaults