@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
@@ -8,11 +8,11 @@
8
8
  import { execFileSync } from 'node:child_process';
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
- import { normalizePath } from './constants.js';
12
- import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from './db.js';
13
- import { isTestFile } from './infrastructure/test-filter.js';
14
- import { warn } from './logger.js';
15
- import { paginateResult } from './paginate.js';
11
+ import { closeDb, findDbPath, initSchema, openDb, openReadonlyOrFail } from '../db/index.js';
12
+ import { warn } from '../infrastructure/logger.js';
13
+ import { isTestFile } from '../infrastructure/test-filter.js';
14
+ import { normalizePath } from '../shared/constants.js';
15
+ import { paginateResult } from '../shared/paginate.js';
16
16
 
17
17
  /**
18
18
  * Scan git history and return parsed commit data.
@@ -1,8 +1,8 @@
1
1
  import path from 'node:path';
2
- import { openReadonlyOrFail } from './db.js';
3
- import { louvainCommunities } from './graph/algorithms/louvain.js';
4
- import { buildDependencyGraph } from './graph/builders/dependency.js';
5
- import { paginateResult } from './paginate.js';
2
+ import { openRepo } from '../db/index.js';
3
+ import { louvainCommunities } from '../graph/algorithms/louvain.js';
4
+ import { buildDependencyGraph } from '../graph/builders/dependency.js';
5
+ import { paginateResult } from '../shared/paginate.js';
6
6
 
7
7
  // ─── Directory Helpers ────────────────────────────────────────────────
8
8
 
@@ -11,48 +11,18 @@ function getDirectory(filePath) {
11
11
  return dir === '.' ? '(root)' : dir;
12
12
  }
13
13
 
14
- // ─── Core Analysis ────────────────────────────────────────────────────
14
+ // ─── Community Building ──────────────────────────────────────────────
15
15
 
16
16
  /**
17
- * Run Louvain community detection and return structured data.
18
- *
19
- * @param {string} [customDbPath] - Path to graph.db
20
- * @param {object} [opts]
21
- * @param {boolean} [opts.functions] - Function-level instead of file-level
22
- * @param {number} [opts.resolution] - Louvain resolution (default 1.0)
23
- * @param {boolean} [opts.noTests] - Exclude test files
24
- * @param {boolean} [opts.drift] - Drift-only mode (omit community member lists)
25
- * @param {boolean} [opts.json] - JSON output (used by CLI wrapper only)
26
- * @returns {{ communities: object[], modularity: number, drift: object, summary: object }}
17
+ * Group graph nodes by Louvain community assignment and build structured objects.
18
+ * @param {object} graph - The dependency graph
19
+ * @param {Map<string, number>} assignments - Node key → community ID
20
+ * @param {object} opts
21
+ * @param {boolean} [opts.drift] - If true, omit member lists
22
+ * @returns {{ communities: object[], communityDirs: Map<number, Set<string>> }}
27
23
  */
28
- export function communitiesData(customDbPath, opts = {}) {
29
- const db = openReadonlyOrFail(customDbPath);
30
- let graph;
31
- try {
32
- graph = buildDependencyGraph(db, {
33
- fileLevel: !opts.functions,
34
- noTests: opts.noTests,
35
- });
36
- } finally {
37
- db.close();
38
- }
39
-
40
- // Handle empty or trivial graphs
41
- if (graph.nodeCount === 0 || graph.edgeCount === 0) {
42
- return {
43
- communities: [],
44
- modularity: 0,
45
- drift: { splitCandidates: [], mergeCandidates: [] },
46
- summary: { communityCount: 0, modularity: 0, nodeCount: graph.nodeCount, driftScore: 0 },
47
- };
48
- }
49
-
50
- // Run Louvain
51
- const resolution = opts.resolution ?? 1.0;
52
- const { assignments, modularity } = louvainCommunities(graph, { resolution });
53
-
54
- // Group nodes by community
55
- const communityMap = new Map(); // community id → node keys[]
24
+ function buildCommunityObjects(graph, assignments, opts) {
25
+ const communityMap = new Map();
56
26
  for (const [key] of graph.nodes()) {
57
27
  const cid = assignments.get(key);
58
28
  if (cid == null) continue;
@@ -60,9 +30,8 @@ export function communitiesData(customDbPath, opts = {}) {
60
30
  communityMap.get(cid).push(key);
61
31
  }
62
32
 
63
- // Build community objects
64
33
  const communities = [];
65
- const communityDirs = new Map(); // community id → Set<dir>
34
+ const communityDirs = new Map();
66
35
 
67
36
  for (const [cid, members] of communityMap) {
68
37
  const dirCounts = {};
@@ -88,19 +57,27 @@ export function communitiesData(customDbPath, opts = {}) {
88
57
  });
89
58
  }
90
59
 
91
- // Sort by size descending
92
60
  communities.sort((a, b) => b.size - a.size);
61
+ return { communities, communityDirs };
62
+ }
93
63
 
94
- // ─── Drift Analysis ─────────────────────────────────────────────
64
+ // ─── Drift Analysis ──────────────────────────────────────────────────
95
65
 
96
- // Split candidates: directories with members in 2+ communities
97
- const dirToCommunities = new Map(); // dir Set<community id>
66
+ /**
67
+ * Compute split/merge candidates and drift score from community directory data.
68
+ * @param {object[]} communities - Community objects with `directories`
69
+ * @param {Map<number, Set<string>>} communityDirs - Community ID → directory set
70
+ * @returns {{ splitCandidates: object[], mergeCandidates: object[], driftScore: number }}
71
+ */
72
+ function analyzeDrift(communities, communityDirs) {
73
+ const dirToCommunities = new Map();
98
74
  for (const [cid, dirs] of communityDirs) {
99
75
  for (const dir of dirs) {
100
76
  if (!dirToCommunities.has(dir)) dirToCommunities.set(dir, new Set());
101
77
  dirToCommunities.get(dir).add(cid);
102
78
  }
103
79
  }
80
+
104
81
  const splitCandidates = [];
105
82
  for (const [dir, cids] of dirToCommunities) {
106
83
  if (cids.size >= 2) {
@@ -109,7 +86,6 @@ export function communitiesData(customDbPath, opts = {}) {
109
86
  }
110
87
  splitCandidates.sort((a, b) => b.communityCount - a.communityCount);
111
88
 
112
- // Merge candidates: communities spanning 2+ directories
113
89
  const mergeCandidates = [];
114
90
  for (const c of communities) {
115
91
  const dirCount = Object.keys(c.directories).length;
@@ -124,17 +100,56 @@ export function communitiesData(customDbPath, opts = {}) {
124
100
  }
125
101
  mergeCandidates.sort((a, b) => b.directoryCount - a.directoryCount);
126
102
 
127
- // Drift score: 0-100 based on how much directory structure diverges from communities
128
103
  const totalDirs = dirToCommunities.size;
129
- const splitDirs = splitCandidates.length;
130
- const splitRatio = totalDirs > 0 ? splitDirs / totalDirs : 0;
131
-
104
+ const splitRatio = totalDirs > 0 ? splitCandidates.length / totalDirs : 0;
132
105
  const totalComms = communities.length;
133
- const mergeComms = mergeCandidates.length;
134
- const mergeRatio = totalComms > 0 ? mergeComms / totalComms : 0;
135
-
106
+ const mergeRatio = totalComms > 0 ? mergeCandidates.length / totalComms : 0;
136
107
  const driftScore = Math.round(((splitRatio + mergeRatio) / 2) * 100);
137
108
 
109
+ return { splitCandidates, mergeCandidates, driftScore };
110
+ }
111
+
112
+ // ─── Core Analysis ────────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Run Louvain community detection and return structured data.
116
+ *
117
+ * @param {string} [customDbPath] - Path to graph.db
118
+ * @param {object} [opts]
119
+ * @param {boolean} [opts.functions] - Function-level instead of file-level
120
+ * @param {number} [opts.resolution] - Louvain resolution (default 1.0)
121
+ * @param {boolean} [opts.noTests] - Exclude test files
122
+ * @param {boolean} [opts.drift] - Drift-only mode (omit community member lists)
123
+ * @param {boolean} [opts.json] - JSON output (used by CLI wrapper only)
124
+ * @returns {{ communities: object[], modularity: number, drift: object, summary: object }}
125
+ */
126
+ export function communitiesData(customDbPath, opts = {}) {
127
+ const { repo, close } = openRepo(customDbPath, opts);
128
+ let graph;
129
+ try {
130
+ graph = buildDependencyGraph(repo, {
131
+ fileLevel: !opts.functions,
132
+ noTests: opts.noTests,
133
+ });
134
+ } finally {
135
+ close();
136
+ }
137
+
138
+ if (graph.nodeCount === 0 || graph.edgeCount === 0) {
139
+ return {
140
+ communities: [],
141
+ modularity: 0,
142
+ drift: { splitCandidates: [], mergeCandidates: [] },
143
+ summary: { communityCount: 0, modularity: 0, nodeCount: graph.nodeCount, driftScore: 0 },
144
+ };
145
+ }
146
+
147
+ const resolution = opts.resolution ?? 1.0;
148
+ const { assignments, modularity } = louvainCommunities(graph, { resolution });
149
+
150
+ const { communities, communityDirs } = buildCommunityObjects(graph, assignments, opts);
151
+ const { splitCandidates, mergeCandidates, driftScore } = analyzeDrift(communities, communityDirs);
152
+
138
153
  const base = {
139
154
  communities: opts.drift ? [] : communities,
140
155
  modularity: +modularity.toFixed(4),
@@ -3,20 +3,21 @@ import path from 'node:path';
3
3
  import {
4
4
  computeLOCMetrics as _computeLOCMetrics,
5
5
  computeMaintainabilityIndex as _computeMaintainabilityIndex,
6
- } from './ast-analysis/metrics.js';
7
- import { COMPLEXITY_RULES, HALSTEAD_RULES } from './ast-analysis/rules/index.js';
6
+ } from '../ast-analysis/metrics.js';
7
+ import { COMPLEXITY_RULES, HALSTEAD_RULES } from '../ast-analysis/rules/index.js';
8
8
  import {
9
9
  findFunctionNode as _findFunctionNode,
10
10
  buildExtensionSet,
11
11
  buildExtToLangMap,
12
- } from './ast-analysis/shared.js';
13
- import { walkWithVisitors } from './ast-analysis/visitor.js';
14
- import { createComplexityVisitor } from './ast-analysis/visitors/complexity-visitor.js';
15
- import { loadConfig } from './config.js';
16
- import { getFunctionNodeId, openReadonlyOrFail } from './db.js';
17
- import { isTestFile } from './infrastructure/test-filter.js';
18
- import { info } from './logger.js';
19
- import { paginateResult } from './paginate.js';
12
+ } from '../ast-analysis/shared.js';
13
+ import { walkWithVisitors } from '../ast-analysis/visitor.js';
14
+ import { createComplexityVisitor } from '../ast-analysis/visitors/complexity-visitor.js';
15
+ import { getFunctionNodeId, openReadonlyOrFail } from '../db/index.js';
16
+ import { buildFileConditionSQL } from '../db/query-builder.js';
17
+ import { loadConfig } from '../infrastructure/config.js';
18
+ import { debug, info } from '../infrastructure/logger.js';
19
+ import { isTestFile } from '../infrastructure/test-filter.js';
20
+ import { paginateResult } from '../shared/paginate.js';
20
21
 
21
22
  // Re-export rules for backward compatibility
22
23
  export { COMPLEXITY_RULES, HALSTEAD_RULES };
@@ -330,42 +331,139 @@ export function computeAllMetrics(functionNode, langId) {
330
331
  */
331
332
  export { _findFunctionNode as findFunctionNode };
332
333
 
333
- /**
334
- * Re-parse changed files with WASM tree-sitter, find function AST subtrees,
335
- * compute complexity, and upsert into function_complexity table.
336
- *
337
- * @param {object} db - open better-sqlite3 database (read-write)
338
- * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, ... }>
339
- * @param {string} rootDir - absolute project root path
340
- * @param {object} [engineOpts] - engine options (unused; always uses WASM for AST)
341
- */
342
- export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOpts) {
343
- // Only initialize WASM parsers if some files lack both a cached tree AND pre-computed complexity
344
- let parsers = null;
345
- let extToLang = null;
346
- let needsFallback = false;
334
+ async function initWasmParsersIfNeeded(fileSymbols) {
347
335
  for (const [relPath, symbols] of fileSymbols) {
348
336
  if (!symbols._tree) {
349
- // Only consider files whose language actually has complexity rules
350
337
  const ext = path.extname(relPath).toLowerCase();
351
338
  if (!COMPLEXITY_EXTENSIONS.has(ext)) continue;
352
- // Check if all function/method defs have pre-computed complexity (native engine)
353
339
  const hasPrecomputed = symbols.definitions.every(
354
340
  (d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
355
341
  );
356
342
  if (!hasPrecomputed) {
357
- needsFallback = true;
358
- break;
343
+ const { createParsers } = await import('../domain/parser.js');
344
+ const parsers = await createParsers();
345
+ const extToLang = buildExtToLangMap();
346
+ return { parsers, extToLang };
359
347
  }
360
348
  }
361
349
  }
362
- if (needsFallback) {
363
- const { createParsers } = await import('./parser.js');
364
- parsers = await createParsers();
365
- extToLang = buildExtToLangMap();
350
+ return { parsers: null, extToLang: null };
351
+ }
352
+
353
+ function getTreeForFile(symbols, relPath, rootDir, parsers, extToLang, getParser) {
354
+ let tree = symbols._tree;
355
+ let langId = symbols._langId;
356
+
357
+ const allPrecomputed = symbols.definitions.every(
358
+ (d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
359
+ );
360
+
361
+ if (!allPrecomputed && !tree) {
362
+ const ext = path.extname(relPath).toLowerCase();
363
+ if (!COMPLEXITY_EXTENSIONS.has(ext)) return null;
364
+ if (!extToLang) return null;
365
+ langId = extToLang.get(ext);
366
+ if (!langId) return null;
367
+
368
+ const absPath = path.join(rootDir, relPath);
369
+ let code;
370
+ try {
371
+ code = fs.readFileSync(absPath, 'utf-8');
372
+ } catch (e) {
373
+ debug(`complexity: cannot read ${relPath}: ${e.message}`);
374
+ return null;
375
+ }
376
+
377
+ const parser = getParser(parsers, absPath);
378
+ if (!parser) return null;
379
+
380
+ try {
381
+ tree = parser.parse(code);
382
+ } catch (e) {
383
+ debug(`complexity: parse failed for ${relPath}: ${e.message}`);
384
+ return null;
385
+ }
366
386
  }
367
387
 
368
- const { getParser } = await import('./parser.js');
388
+ return { tree, langId };
389
+ }
390
+
391
+ function upsertPrecomputedComplexity(db, upsert, def, relPath) {
392
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
393
+ if (!nodeId) return 0;
394
+ const ch = def.complexity.halstead;
395
+ const cl = def.complexity.loc;
396
+ upsert.run(
397
+ nodeId,
398
+ def.complexity.cognitive,
399
+ def.complexity.cyclomatic,
400
+ def.complexity.maxNesting ?? 0,
401
+ cl ? cl.loc : 0,
402
+ cl ? cl.sloc : 0,
403
+ cl ? cl.commentLines : 0,
404
+ ch ? ch.n1 : 0,
405
+ ch ? ch.n2 : 0,
406
+ ch ? ch.bigN1 : 0,
407
+ ch ? ch.bigN2 : 0,
408
+ ch ? ch.vocabulary : 0,
409
+ ch ? ch.length : 0,
410
+ ch ? ch.volume : 0,
411
+ ch ? ch.difficulty : 0,
412
+ ch ? ch.effort : 0,
413
+ ch ? ch.bugs : 0,
414
+ def.complexity.maintainabilityIndex ?? 0,
415
+ );
416
+ return 1;
417
+ }
418
+
419
+ function upsertAstComplexity(db, upsert, def, relPath, tree, langId, rules) {
420
+ if (!tree || !rules) return 0;
421
+
422
+ const funcNode = _findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
423
+ if (!funcNode) return 0;
424
+
425
+ const metrics = computeAllMetrics(funcNode, langId);
426
+ if (!metrics) return 0;
427
+
428
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
429
+ if (!nodeId) return 0;
430
+
431
+ const h = metrics.halstead;
432
+ upsert.run(
433
+ nodeId,
434
+ metrics.cognitive,
435
+ metrics.cyclomatic,
436
+ metrics.maxNesting,
437
+ metrics.loc.loc,
438
+ metrics.loc.sloc,
439
+ metrics.loc.commentLines,
440
+ h ? h.n1 : 0,
441
+ h ? h.n2 : 0,
442
+ h ? h.bigN1 : 0,
443
+ h ? h.bigN2 : 0,
444
+ h ? h.vocabulary : 0,
445
+ h ? h.length : 0,
446
+ h ? h.volume : 0,
447
+ h ? h.difficulty : 0,
448
+ h ? h.effort : 0,
449
+ h ? h.bugs : 0,
450
+ metrics.mi,
451
+ );
452
+ return 1;
453
+ }
454
+
455
+ /**
456
+ * Re-parse changed files with WASM tree-sitter, find function AST subtrees,
457
+ * compute complexity, and upsert into function_complexity table.
458
+ *
459
+ * @param {object} db - open better-sqlite3 database (read-write)
460
+ * @param {Map<string, object>} fileSymbols - Map<relPath, { definitions, ... }>
461
+ * @param {string} rootDir - absolute project root path
462
+ * @param {object} [engineOpts] - engine options (unused; always uses WASM for AST)
463
+ */
464
+ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOpts) {
465
+ const { parsers, extToLang } = await initWasmParsersIfNeeded(fileSymbols);
466
+ const { getParser } = await import('../domain/parser.js');
369
467
 
370
468
  const upsert = db.prepare(
371
469
  `INSERT OR REPLACE INTO function_complexity
@@ -381,39 +479,9 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
381
479
 
382
480
  const tx = db.transaction(() => {
383
481
  for (const [relPath, symbols] of fileSymbols) {
384
- // Check if all function/method defs have pre-computed complexity
385
- const allPrecomputed = symbols.definitions.every(
386
- (d) => (d.kind !== 'function' && d.kind !== 'method') || d.complexity,
387
- );
388
-
389
- let tree = symbols._tree;
390
- let langId = symbols._langId;
391
-
392
- // Only attempt WASM fallback if we actually need AST-based computation
393
- if (!allPrecomputed && !tree) {
394
- const ext = path.extname(relPath).toLowerCase();
395
- if (!COMPLEXITY_EXTENSIONS.has(ext)) continue; // Language has no complexity rules
396
- if (!extToLang) continue; // No WASM parsers available
397
- langId = extToLang.get(ext);
398
- if (!langId) continue;
399
-
400
- const absPath = path.join(rootDir, relPath);
401
- let code;
402
- try {
403
- code = fs.readFileSync(absPath, 'utf-8');
404
- } catch {
405
- continue;
406
- }
407
-
408
- const parser = getParser(parsers, absPath);
409
- if (!parser) continue;
410
-
411
- try {
412
- tree = parser.parse(code);
413
- } catch {
414
- continue;
415
- }
416
- }
482
+ const result = getTreeForFile(symbols, relPath, rootDir, parsers, extToLang, getParser);
483
+ const tree = result ? result.tree : null;
484
+ const langId = result ? result.langId : null;
417
485
 
418
486
  const rules = langId ? COMPLEXITY_RULES.get(langId) : null;
419
487
 
@@ -421,71 +489,11 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp
421
489
  if (def.kind !== 'function' && def.kind !== 'method') continue;
422
490
  if (!def.line) continue;
423
491
 
424
- // Use pre-computed complexity from native engine if available
425
492
  if (def.complexity) {
426
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
427
- if (!nodeId) continue;
428
- const ch = def.complexity.halstead;
429
- const cl = def.complexity.loc;
430
- upsert.run(
431
- nodeId,
432
- def.complexity.cognitive,
433
- def.complexity.cyclomatic,
434
- def.complexity.maxNesting ?? 0,
435
- cl ? cl.loc : 0,
436
- cl ? cl.sloc : 0,
437
- cl ? cl.commentLines : 0,
438
- ch ? ch.n1 : 0,
439
- ch ? ch.n2 : 0,
440
- ch ? ch.bigN1 : 0,
441
- ch ? ch.bigN2 : 0,
442
- ch ? ch.vocabulary : 0,
443
- ch ? ch.length : 0,
444
- ch ? ch.volume : 0,
445
- ch ? ch.difficulty : 0,
446
- ch ? ch.effort : 0,
447
- ch ? ch.bugs : 0,
448
- def.complexity.maintainabilityIndex ?? 0,
449
- );
450
- analyzed++;
451
- continue;
493
+ analyzed += upsertPrecomputedComplexity(db, upsert, def, relPath);
494
+ } else {
495
+ analyzed += upsertAstComplexity(db, upsert, def, relPath, tree, langId, rules);
452
496
  }
453
-
454
- // Fallback: compute from AST tree
455
- if (!tree || !rules) continue;
456
-
457
- const funcNode = _findFunctionNode(tree.rootNode, def.line, def.endLine, rules);
458
- if (!funcNode) continue;
459
-
460
- // Single-pass: complexity + Halstead + LOC + MI in one DFS walk
461
- const metrics = computeAllMetrics(funcNode, langId);
462
- if (!metrics) continue;
463
-
464
- const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
465
- if (!nodeId) continue;
466
-
467
- const h = metrics.halstead;
468
- upsert.run(
469
- nodeId,
470
- metrics.cognitive,
471
- metrics.cyclomatic,
472
- metrics.maxNesting,
473
- metrics.loc.loc,
474
- metrics.loc.sloc,
475
- metrics.loc.commentLines,
476
- h ? h.n1 : 0,
477
- h ? h.n2 : 0,
478
- h ? h.bigN1 : 0,
479
- h ? h.bigN2 : 0,
480
- h ? h.vocabulary : 0,
481
- h ? h.length : 0,
482
- h ? h.volume : 0,
483
- h ? h.difficulty : 0,
484
- h ? h.effort : 0,
485
- h ? h.bugs : 0,
486
- metrics.mi,
487
- );
488
- analyzed++;
489
497
  }
490
498
  }
491
499
  });
@@ -524,7 +532,7 @@ export function complexityData(customDbPath, opts = {}) {
524
532
  const kindFilter = opts.kind || null;
525
533
 
526
534
  // Load thresholds from config
527
- const config = loadConfig(process.cwd());
535
+ const config = opts.config || loadConfig(process.cwd());
528
536
  const thresholds = config.manifesto?.rules || {
529
537
  cognitive: { warn: 15, fail: null },
530
538
  cyclomatic: { warn: 10, fail: null },
@@ -547,9 +555,10 @@ export function complexityData(customDbPath, opts = {}) {
547
555
  where += ' AND n.name LIKE ?';
548
556
  params.push(`%${target}%`);
549
557
  }
550
- if (fileFilter) {
551
- where += ' AND n.file LIKE ?';
552
- params.push(`%${fileFilter}%`);
558
+ {
559
+ const fc = buildFileConditionSQL(fileFilter, 'n.file');
560
+ where += fc.sql;
561
+ params.push(...fc.params);
553
562
  }
554
563
  if (kindFilter) {
555
564
  where += ' AND n.kind = ?';
@@ -606,13 +615,14 @@ export function complexityData(customDbPath, opts = {}) {
606
615
  ORDER BY ${orderBy}`,
607
616
  )
608
617
  .all(...params);
609
- } catch {
618
+ } catch (e) {
619
+ debug(`complexity query failed (table may not exist): ${e.message}`);
610
620
  // Check if graph has nodes even though complexity table is missing/empty
611
621
  let hasGraph = false;
612
622
  try {
613
623
  hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
614
- } catch {
615
- /* ignore */
624
+ } catch (e2) {
625
+ debug(`nodes table check failed: ${e2.message}`);
616
626
  }
617
627
  return { functions: [], summary: null, thresholds, hasGraph };
618
628
  }
@@ -701,8 +711,8 @@ export function complexityData(customDbPath, opts = {}) {
701
711
  ).length,
702
712
  };
703
713
  }
704
- } catch {
705
- /* ignore */
714
+ } catch (e) {
715
+ debug(`complexity summary query failed: ${e.message}`);
706
716
  }
707
717
 
708
718
  // When summary is null (no complexity rows), check if graph has nodes
@@ -710,8 +720,8 @@ export function complexityData(customDbPath, opts = {}) {
710
720
  if (summary === null) {
711
721
  try {
712
722
  hasGraph = db.prepare('SELECT COUNT(*) as c FROM nodes').get().c > 0;
713
- } catch {
714
- /* ignore */
723
+ } catch (e) {
724
+ debug(`nodes table check failed: ${e.message}`);
715
725
  }
716
726
  }
717
727
 
@@ -753,9 +763,10 @@ export function* iterComplexity(customDbPath, opts = {}) {
753
763
  where += ' AND n.name LIKE ?';
754
764
  params.push(`%${opts.target}%`);
755
765
  }
756
- if (opts.file) {
757
- where += ' AND n.file LIKE ?';
758
- params.push(`%${opts.file}%`);
766
+ {
767
+ const fc = buildFileConditionSQL(opts.file, 'n.file');
768
+ where += fc.sql;
769
+ params.push(...fc.params);
759
770
  }
760
771
  if (opts.kind) {
761
772
  where += ' AND n.kind = ?';