@optave/codegraph 3.1.3 → 3.1.5

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 (232) hide show
  1. package/README.md +38 -84
  2. package/package.json +13 -8
  3. package/src/ast-analysis/engine.js +32 -12
  4. package/src/ast-analysis/shared.js +6 -5
  5. package/src/cli/commands/ast.js +22 -0
  6. package/src/cli/commands/audit.js +45 -0
  7. package/src/cli/commands/batch.js +68 -0
  8. package/src/cli/commands/branch-compare.js +21 -0
  9. package/src/cli/commands/build.js +26 -0
  10. package/src/cli/commands/cfg.js +26 -0
  11. package/src/cli/commands/check.js +74 -0
  12. package/src/cli/commands/children.js +28 -0
  13. package/src/cli/commands/co-change.js +67 -0
  14. package/src/cli/commands/communities.js +19 -0
  15. package/src/cli/commands/complexity.js +46 -0
  16. package/src/cli/commands/context.js +30 -0
  17. package/src/cli/commands/cycles.js +32 -0
  18. package/src/cli/commands/dataflow.js +28 -0
  19. package/src/cli/commands/deps.js +12 -0
  20. package/src/cli/commands/diff-impact.js +26 -0
  21. package/src/cli/commands/embed.js +30 -0
  22. package/src/cli/commands/export.js +78 -0
  23. package/src/cli/commands/exports.js +14 -0
  24. package/src/cli/commands/flow.js +32 -0
  25. package/src/cli/commands/fn-impact.js +26 -0
  26. package/src/cli/commands/impact.js +12 -0
  27. package/src/cli/commands/info.js +76 -0
  28. package/src/cli/commands/map.js +19 -0
  29. package/src/cli/commands/mcp.js +18 -0
  30. package/src/cli/commands/models.js +19 -0
  31. package/src/cli/commands/owners.js +25 -0
  32. package/src/cli/commands/path.js +36 -0
  33. package/src/cli/commands/plot.js +89 -0
  34. package/src/cli/commands/query.js +45 -0
  35. package/src/cli/commands/registry.js +100 -0
  36. package/src/cli/commands/roles.js +30 -0
  37. package/src/cli/commands/search.js +42 -0
  38. package/src/cli/commands/sequence.js +28 -0
  39. package/src/cli/commands/snapshot.js +66 -0
  40. package/src/cli/commands/stats.js +15 -0
  41. package/src/cli/commands/structure.js +33 -0
  42. package/src/cli/commands/triage.js +78 -0
  43. package/src/cli/commands/watch.js +12 -0
  44. package/src/cli/commands/where.js +20 -0
  45. package/src/cli/index.js +124 -0
  46. package/src/cli/shared/open-graph.js +13 -0
  47. package/src/cli/shared/options.js +59 -0
  48. package/src/cli/shared/output.js +1 -0
  49. package/src/cli.js +11 -1522
  50. package/src/db/connection.js +130 -7
  51. package/src/{db.js → db/index.js} +17 -5
  52. package/src/db/migrations.js +42 -1
  53. package/src/db/query-builder.js +20 -12
  54. package/src/db/repository/base.js +201 -0
  55. package/src/db/repository/graph-read.js +7 -4
  56. package/src/db/repository/in-memory-repository.js +575 -0
  57. package/src/db/repository/index.js +5 -1
  58. package/src/db/repository/nodes.js +60 -6
  59. package/src/db/repository/sqlite-repository.js +219 -0
  60. package/src/domain/analysis/context.js +408 -0
  61. package/src/domain/analysis/dependencies.js +341 -0
  62. package/src/domain/analysis/exports.js +134 -0
  63. package/src/domain/analysis/impact.js +466 -0
  64. package/src/domain/analysis/module-map.js +322 -0
  65. package/src/domain/analysis/roles.js +45 -0
  66. package/src/domain/analysis/symbol-lookup.js +238 -0
  67. package/src/domain/graph/builder/context.js +85 -0
  68. package/src/domain/graph/builder/helpers.js +218 -0
  69. package/src/domain/graph/builder/incremental.js +178 -0
  70. package/src/domain/graph/builder/pipeline.js +130 -0
  71. package/src/domain/graph/builder/stages/build-edges.js +297 -0
  72. package/src/domain/graph/builder/stages/build-structure.js +113 -0
  73. package/src/domain/graph/builder/stages/collect-files.js +44 -0
  74. package/src/domain/graph/builder/stages/detect-changes.js +413 -0
  75. package/src/domain/graph/builder/stages/finalize.js +139 -0
  76. package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
  77. package/src/domain/graph/builder/stages/parse-files.js +28 -0
  78. package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
  79. package/src/domain/graph/builder/stages/run-analyses.js +44 -0
  80. package/src/domain/graph/builder.js +11 -0
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/domain/graph/cycles.js +82 -0
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +3 -3
  85. package/src/{watcher.js → domain/graph/watcher.js} +10 -150
  86. package/src/{parser.js → domain/parser.js} +5 -5
  87. package/src/domain/queries.js +48 -0
  88. package/src/domain/search/generator.js +163 -0
  89. package/src/domain/search/index.js +13 -0
  90. package/src/domain/search/models.js +218 -0
  91. package/src/domain/search/search/cli-formatter.js +151 -0
  92. package/src/domain/search/search/filters.js +46 -0
  93. package/src/domain/search/search/hybrid.js +121 -0
  94. package/src/domain/search/search/keyword.js +68 -0
  95. package/src/domain/search/search/prepare.js +66 -0
  96. package/src/domain/search/search/semantic.js +145 -0
  97. package/src/domain/search/stores/fts5.js +27 -0
  98. package/src/domain/search/stores/sqlite-blob.js +24 -0
  99. package/src/domain/search/strategies/source.js +14 -0
  100. package/src/domain/search/strategies/structured.js +43 -0
  101. package/src/domain/search/strategies/text-utils.js +43 -0
  102. package/src/extractors/csharp.js +10 -2
  103. package/src/extractors/go.js +3 -1
  104. package/src/extractors/helpers.js +71 -0
  105. package/src/extractors/java.js +9 -2
  106. package/src/extractors/javascript.js +39 -2
  107. package/src/extractors/php.js +3 -1
  108. package/src/extractors/python.js +14 -3
  109. package/src/extractors/rust.js +3 -1
  110. package/src/{ast.js → features/ast.js} +8 -8
  111. package/src/{audit.js → features/audit.js} +16 -44
  112. package/src/{batch.js → features/batch.js} +6 -5
  113. package/src/{boundaries.js → features/boundaries.js} +2 -2
  114. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  115. package/src/{cfg.js → features/cfg.js} +11 -12
  116. package/src/{check.js → features/check.js} +13 -30
  117. package/src/{cochange.js → features/cochange.js} +5 -5
  118. package/src/{communities.js → features/communities.js} +18 -90
  119. package/src/{complexity.js → features/complexity.js} +13 -13
  120. package/src/{dataflow.js → features/dataflow.js} +12 -13
  121. package/src/features/export.js +378 -0
  122. package/src/{flow.js → features/flow.js} +4 -4
  123. package/src/features/graph-enrichment.js +327 -0
  124. package/src/{manifesto.js → features/manifesto.js} +6 -6
  125. package/src/{owners.js → features/owners.js} +2 -2
  126. package/src/{sequence.js → features/sequence.js} +16 -52
  127. package/src/{snapshot.js → features/snapshot.js} +8 -7
  128. package/src/{structure.js → features/structure.js} +20 -45
  129. package/src/{triage.js → features/triage.js} +27 -79
  130. package/src/graph/algorithms/bfs.js +49 -0
  131. package/src/graph/algorithms/centrality.js +16 -0
  132. package/src/graph/algorithms/index.js +5 -0
  133. package/src/graph/algorithms/louvain.js +26 -0
  134. package/src/graph/algorithms/shortest-path.js +41 -0
  135. package/src/graph/algorithms/tarjan.js +49 -0
  136. package/src/graph/builders/dependency.js +110 -0
  137. package/src/graph/builders/index.js +3 -0
  138. package/src/graph/builders/structure.js +40 -0
  139. package/src/graph/builders/temporal.js +33 -0
  140. package/src/graph/classifiers/index.js +2 -0
  141. package/src/graph/classifiers/risk.js +85 -0
  142. package/src/graph/classifiers/roles.js +64 -0
  143. package/src/graph/index.js +13 -0
  144. package/src/graph/model.js +230 -0
  145. package/src/index.cjs +16 -0
  146. package/src/index.js +42 -219
  147. package/src/{native.js → infrastructure/native.js} +3 -1
  148. package/src/infrastructure/result-formatter.js +2 -21
  149. package/src/mcp/index.js +2 -0
  150. package/src/mcp/middleware.js +26 -0
  151. package/src/mcp/server.js +128 -0
  152. package/src/{mcp.js → mcp/tool-registry.js} +6 -675
  153. package/src/mcp/tools/ast-query.js +14 -0
  154. package/src/mcp/tools/audit.js +21 -0
  155. package/src/mcp/tools/batch-query.js +11 -0
  156. package/src/mcp/tools/branch-compare.js +12 -0
  157. package/src/mcp/tools/cfg.js +21 -0
  158. package/src/mcp/tools/check.js +43 -0
  159. package/src/mcp/tools/co-changes.js +20 -0
  160. package/src/mcp/tools/code-owners.js +12 -0
  161. package/src/mcp/tools/communities.js +15 -0
  162. package/src/mcp/tools/complexity.js +18 -0
  163. package/src/mcp/tools/context.js +17 -0
  164. package/src/mcp/tools/dataflow.js +26 -0
  165. package/src/mcp/tools/diff-impact.js +24 -0
  166. package/src/mcp/tools/execution-flow.js +26 -0
  167. package/src/mcp/tools/export-graph.js +57 -0
  168. package/src/mcp/tools/file-deps.js +12 -0
  169. package/src/mcp/tools/file-exports.js +13 -0
  170. package/src/mcp/tools/find-cycles.js +15 -0
  171. package/src/mcp/tools/fn-impact.js +15 -0
  172. package/src/mcp/tools/impact-analysis.js +12 -0
  173. package/src/mcp/tools/index.js +71 -0
  174. package/src/mcp/tools/list-functions.js +14 -0
  175. package/src/mcp/tools/list-repos.js +11 -0
  176. package/src/mcp/tools/module-map.js +6 -0
  177. package/src/mcp/tools/node-roles.js +14 -0
  178. package/src/mcp/tools/path.js +12 -0
  179. package/src/mcp/tools/query.js +30 -0
  180. package/src/mcp/tools/semantic-search.js +65 -0
  181. package/src/mcp/tools/sequence.js +17 -0
  182. package/src/mcp/tools/structure.js +15 -0
  183. package/src/mcp/tools/symbol-children.js +14 -0
  184. package/src/mcp/tools/triage.js +35 -0
  185. package/src/mcp/tools/where.js +13 -0
  186. package/src/{commands → presentation}/audit.js +2 -2
  187. package/src/{commands → presentation}/batch.js +1 -1
  188. package/src/{commands → presentation}/branch-compare.js +2 -2
  189. package/src/{commands → presentation}/cfg.js +1 -1
  190. package/src/{commands → presentation}/check.js +6 -6
  191. package/src/presentation/colors.js +44 -0
  192. package/src/{commands → presentation}/communities.js +1 -1
  193. package/src/{commands → presentation}/complexity.js +1 -1
  194. package/src/{commands → presentation}/dataflow.js +1 -1
  195. package/src/presentation/export.js +444 -0
  196. package/src/{commands → presentation}/flow.js +2 -2
  197. package/src/{commands → presentation}/manifesto.js +4 -4
  198. package/src/{commands → presentation}/owners.js +1 -1
  199. package/src/presentation/queries-cli/exports.js +46 -0
  200. package/src/presentation/queries-cli/impact.js +198 -0
  201. package/src/presentation/queries-cli/index.js +5 -0
  202. package/src/presentation/queries-cli/inspect.js +334 -0
  203. package/src/presentation/queries-cli/overview.js +197 -0
  204. package/src/presentation/queries-cli/path.js +58 -0
  205. package/src/presentation/queries-cli.js +27 -0
  206. package/src/{commands → presentation}/query.js +1 -1
  207. package/src/presentation/result-formatter.js +144 -0
  208. package/src/presentation/sequence-renderer.js +43 -0
  209. package/src/{commands → presentation}/sequence.js +2 -2
  210. package/src/{commands → presentation}/structure.js +2 -2
  211. package/src/presentation/table.js +47 -0
  212. package/src/{commands → presentation}/triage.js +1 -1
  213. package/src/{viewer.js → presentation/viewer.js} +68 -382
  214. package/src/{constants.js → shared/constants.js} +1 -1
  215. package/src/shared/errors.js +78 -0
  216. package/src/shared/file-utils.js +153 -0
  217. package/src/shared/generators.js +125 -0
  218. package/src/shared/hierarchy.js +27 -0
  219. package/src/shared/normalize.js +59 -0
  220. package/src/builder.js +0 -1486
  221. package/src/cycles.js +0 -137
  222. package/src/embedder.js +0 -1097
  223. package/src/export.js +0 -681
  224. package/src/queries-cli.js +0 -866
  225. package/src/queries.js +0 -2289
  226. /package/src/{config.js → infrastructure/config.js} +0 -0
  227. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  228. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  229. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  230. /package/src/{commands → presentation}/cochange.js +0 -0
  231. /package/src/{kinds.js → shared/kinds.js} +0 -0
  232. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,327 @@
1
+ import path from 'node:path';
2
+ import { louvainCommunities } from '../graph/algorithms/louvain.js';
3
+ import { CodeGraph } from '../graph/model.js';
4
+ import { isTestFile } from '../infrastructure/test-filter.js';
5
+ import {
6
+ COMMUNITY_COLORS,
7
+ DEFAULT_NODE_COLORS,
8
+ DEFAULT_ROLE_COLORS,
9
+ } from '../presentation/colors.js';
10
+ import { DEFAULT_CONFIG, renderPlotHTML } from '../presentation/viewer.js';
11
+
12
+ // Re-export presentation utilities for backward compatibility
13
+ export { loadPlotConfig } from '../presentation/viewer.js';
14
+
15
+ const DEFAULT_MIN_CONFIDENCE = 0.5;
16
+
17
+ // ─── Data Preparation ─────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Prepare enriched graph data for the HTML viewer.
21
+ */
22
+ export function prepareGraphData(db, opts = {}) {
23
+ const fileLevel = opts.fileLevel !== false;
24
+ const noTests = opts.noTests || false;
25
+ const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
26
+ const cfg = opts.config || DEFAULT_CONFIG;
27
+
28
+ return fileLevel
29
+ ? prepareFileLevelData(db, noTests, minConf, cfg)
30
+ : prepareFunctionLevelData(db, noTests, minConf, cfg);
31
+ }
32
+
33
+ function prepareFunctionLevelData(db, noTests, minConf, cfg) {
34
+ let edges = db
35
+ .prepare(
36
+ `
37
+ SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
38
+ n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
39
+ n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
40
+ n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
41
+ e.kind AS edge_kind
42
+ FROM edges e
43
+ JOIN nodes n1 ON e.source_id = n1.id
44
+ JOIN nodes n2 ON e.target_id = n2.id
45
+ WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
46
+ AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
47
+ AND e.kind = 'calls'
48
+ AND e.confidence >= ?
49
+ `,
50
+ )
51
+ .all(minConf);
52
+ if (noTests)
53
+ edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
54
+
55
+ if (cfg.filter.kinds) {
56
+ const kinds = new Set(cfg.filter.kinds);
57
+ edges = edges.filter((e) => kinds.has(e.source_kind) && kinds.has(e.target_kind));
58
+ }
59
+ if (cfg.filter.files) {
60
+ const patterns = cfg.filter.files;
61
+ edges = edges.filter(
62
+ (e) =>
63
+ patterns.some((p) => e.source_file.includes(p)) &&
64
+ patterns.some((p) => e.target_file.includes(p)),
65
+ );
66
+ }
67
+
68
+ const nodeMap = new Map();
69
+ for (const e of edges) {
70
+ if (!nodeMap.has(e.source_id)) {
71
+ nodeMap.set(e.source_id, {
72
+ id: e.source_id,
73
+ name: e.source_name,
74
+ kind: e.source_kind,
75
+ file: e.source_file,
76
+ line: e.source_line,
77
+ role: e.source_role,
78
+ });
79
+ }
80
+ if (!nodeMap.has(e.target_id)) {
81
+ nodeMap.set(e.target_id, {
82
+ id: e.target_id,
83
+ name: e.target_name,
84
+ kind: e.target_kind,
85
+ file: e.target_file,
86
+ line: e.target_line,
87
+ role: e.target_role,
88
+ });
89
+ }
90
+ }
91
+
92
+ if (cfg.filter.roles) {
93
+ const roles = new Set(cfg.filter.roles);
94
+ for (const [id, n] of nodeMap) {
95
+ if (!roles.has(n.role)) nodeMap.delete(id);
96
+ }
97
+ const nodeIds = new Set(nodeMap.keys());
98
+ edges = edges.filter((e) => nodeIds.has(e.source_id) && nodeIds.has(e.target_id));
99
+ }
100
+
101
+ // Complexity data
102
+ const complexityMap = new Map();
103
+ try {
104
+ const rows = db
105
+ .prepare(
106
+ 'SELECT node_id, cognitive, cyclomatic, max_nesting, maintainability_index FROM function_complexity',
107
+ )
108
+ .all();
109
+ for (const r of rows) {
110
+ complexityMap.set(r.node_id, {
111
+ cognitive: r.cognitive,
112
+ cyclomatic: r.cyclomatic,
113
+ maintainabilityIndex: r.maintainability_index,
114
+ });
115
+ }
116
+ } catch {
117
+ // table may not exist in old DBs
118
+ }
119
+
120
+ // Fan-in / fan-out via graph subsystem
121
+ const fnGraph = new CodeGraph();
122
+ for (const [id] of nodeMap) fnGraph.addNode(String(id));
123
+ for (const e of edges) {
124
+ const src = String(e.source_id);
125
+ const tgt = String(e.target_id);
126
+ if (src !== tgt && !fnGraph.hasEdge(src, tgt)) fnGraph.addEdge(src, tgt);
127
+ }
128
+
129
+ // Use DB-level fan-in/fan-out (counts ALL call edges, not just visible)
130
+ const fanInMap = new Map();
131
+ const fanOutMap = new Map();
132
+ const fanInRows = db
133
+ .prepare(
134
+ "SELECT target_id AS node_id, COUNT(*) AS fan_in FROM edges WHERE kind = 'calls' GROUP BY target_id",
135
+ )
136
+ .all();
137
+ for (const r of fanInRows) fanInMap.set(r.node_id, r.fan_in);
138
+
139
+ const fanOutRows = db
140
+ .prepare(
141
+ "SELECT source_id AS node_id, COUNT(*) AS fan_out FROM edges WHERE kind = 'calls' GROUP BY source_id",
142
+ )
143
+ .all();
144
+ for (const r of fanOutRows) fanOutMap.set(r.node_id, r.fan_out);
145
+
146
+ // Communities (Louvain) via graph subsystem
147
+ const communityMap = new Map();
148
+ if (nodeMap.size > 0) {
149
+ try {
150
+ const { assignments } = louvainCommunities(fnGraph);
151
+ for (const [nid, cid] of assignments) communityMap.set(Number(nid), cid);
152
+ } catch {
153
+ // louvain can fail on disconnected graphs
154
+ }
155
+ }
156
+
157
+ // Build enriched nodes
158
+ const visNodes = [...nodeMap.values()].map((n) => {
159
+ const cx = complexityMap.get(n.id) || null;
160
+ const fanIn = fanInMap.get(n.id) || 0;
161
+ const fanOut = fanOutMap.get(n.id) || 0;
162
+ const community = communityMap.get(n.id) ?? null;
163
+ const directory = path.dirname(n.file);
164
+ const risk = [];
165
+ if (n.role === 'dead') risk.push('dead-code');
166
+ if (fanIn >= (cfg.riskThresholds?.highBlastRadius ?? 10)) risk.push('high-blast-radius');
167
+ if (cx && cx.maintainabilityIndex < (cfg.riskThresholds?.lowMI ?? 40)) risk.push('low-mi');
168
+
169
+ const color =
170
+ cfg.colorBy === 'role' && n.role
171
+ ? cfg.roleColors[n.role] || DEFAULT_ROLE_COLORS[n.role] || '#ccc'
172
+ : cfg.colorBy === 'community' && community !== null
173
+ ? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length]
174
+ : cfg.nodeColors[n.kind] || DEFAULT_NODE_COLORS[n.kind] || '#ccc';
175
+
176
+ return {
177
+ id: n.id,
178
+ label: n.name,
179
+ title: `${n.file}:${n.line} (${n.kind}${n.role ? `, ${n.role}` : ''})`,
180
+ color,
181
+ kind: n.kind,
182
+ role: n.role || '',
183
+ file: n.file,
184
+ line: n.line,
185
+ community,
186
+ cognitive: cx?.cognitive ?? null,
187
+ cyclomatic: cx?.cyclomatic ?? null,
188
+ maintainabilityIndex: cx?.maintainabilityIndex ?? null,
189
+ fanIn,
190
+ fanOut,
191
+ directory,
192
+ risk,
193
+ };
194
+ });
195
+
196
+ const visEdges = edges.map((e, i) => ({
197
+ id: `e${i}`,
198
+ from: e.source_id,
199
+ to: e.target_id,
200
+ }));
201
+
202
+ // Seed strategy
203
+ let seedNodeIds;
204
+ if (cfg.seedStrategy === 'top-fanin') {
205
+ const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
206
+ seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
207
+ } else if (cfg.seedStrategy === 'entry') {
208
+ seedNodeIds = visNodes.filter((n) => n.role === 'entry').map((n) => n.id);
209
+ } else {
210
+ seedNodeIds = visNodes.map((n) => n.id);
211
+ }
212
+
213
+ return { nodes: visNodes, edges: visEdges, seedNodeIds };
214
+ }
215
+
216
+ function prepareFileLevelData(db, noTests, minConf, cfg) {
217
+ let edges = db
218
+ .prepare(
219
+ `
220
+ SELECT DISTINCT n1.file AS source, n2.file AS target
221
+ FROM edges e
222
+ JOIN nodes n1 ON e.source_id = n1.id
223
+ JOIN nodes n2 ON e.target_id = n2.id
224
+ WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
225
+ AND e.confidence >= ?
226
+ `,
227
+ )
228
+ .all(minConf);
229
+ if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
230
+
231
+ const files = new Set();
232
+ for (const { source, target } of edges) {
233
+ files.add(source);
234
+ files.add(target);
235
+ }
236
+
237
+ const fileIds = new Map();
238
+ let idx = 0;
239
+ for (const f of files) fileIds.set(f, idx++);
240
+
241
+ // Fan-in/fan-out
242
+ const fanInCount = new Map();
243
+ const fanOutCount = new Map();
244
+ for (const { source, target } of edges) {
245
+ fanOutCount.set(source, (fanOutCount.get(source) || 0) + 1);
246
+ fanInCount.set(target, (fanInCount.get(target) || 0) + 1);
247
+ }
248
+
249
+ // Communities via graph subsystem
250
+ const communityMap = new Map();
251
+ if (files.size > 0) {
252
+ try {
253
+ const fileGraph = new CodeGraph();
254
+ for (const f of files) fileGraph.addNode(f);
255
+ for (const { source, target } of edges) {
256
+ if (source !== target && !fileGraph.hasEdge(source, target))
257
+ fileGraph.addEdge(source, target);
258
+ }
259
+ const { assignments } = louvainCommunities(fileGraph);
260
+ for (const [file, cid] of assignments) communityMap.set(file, cid);
261
+ } catch {
262
+ // ignore
263
+ }
264
+ }
265
+
266
+ const visNodes = [...files].map((f) => {
267
+ const id = fileIds.get(f);
268
+ const community = communityMap.get(f) ?? null;
269
+ const fanIn = fanInCount.get(f) || 0;
270
+ const fanOut = fanOutCount.get(f) || 0;
271
+ const directory = path.dirname(f);
272
+ const color =
273
+ cfg.colorBy === 'community' && community !== null
274
+ ? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length]
275
+ : cfg.nodeColors.file || DEFAULT_NODE_COLORS.file;
276
+
277
+ return {
278
+ id,
279
+ label: path.basename(f),
280
+ title: f,
281
+ color,
282
+ kind: 'file',
283
+ role: '',
284
+ file: f,
285
+ line: 0,
286
+ community,
287
+ cognitive: null,
288
+ cyclomatic: null,
289
+ maintainabilityIndex: null,
290
+ fanIn,
291
+ fanOut,
292
+ directory,
293
+ risk: [],
294
+ };
295
+ });
296
+
297
+ const visEdges = edges.map(({ source, target }, i) => ({
298
+ id: `e${i}`,
299
+ from: fileIds.get(source),
300
+ to: fileIds.get(target),
301
+ }));
302
+
303
+ let seedNodeIds;
304
+ if (cfg.seedStrategy === 'top-fanin') {
305
+ const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
306
+ seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
307
+ } else if (cfg.seedStrategy === 'entry') {
308
+ seedNodeIds = visNodes.map((n) => n.id);
309
+ } else {
310
+ seedNodeIds = visNodes.map((n) => n.id);
311
+ }
312
+
313
+ return { nodes: visNodes, edges: visEdges, seedNodeIds };
314
+ }
315
+
316
+ // ─── HTML Generation (thin wrapper) ──────────────────────────────────
317
+
318
+ /**
319
+ * Generate a self-contained interactive HTML file with vis-network.
320
+ *
321
+ * Loads graph data from the DB, then delegates to the presentation layer.
322
+ */
323
+ export function generatePlotHTML(db, opts = {}) {
324
+ const cfg = opts.config || DEFAULT_CONFIG;
325
+ const data = prepareGraphData(db, opts);
326
+ return renderPlotHTML(data, cfg);
327
+ }
@@ -1,9 +1,9 @@
1
+ import { openReadonlyOrFail } from '../db/index.js';
2
+ import { findCycles } from '../domain/graph/cycles.js';
3
+ import { loadConfig } from '../infrastructure/config.js';
4
+ import { debug } from '../infrastructure/logger.js';
5
+ import { paginateResult } from '../shared/paginate.js';
1
6
  import { evaluateBoundaries } from './boundaries.js';
2
- import { loadConfig } from './config.js';
3
- import { findCycles } from './cycles.js';
4
- import { openReadonlyOrFail } from './db.js';
5
- import { debug } from './logger.js';
6
- import { paginateResult } from './paginate.js';
7
7
 
8
8
  // ─── Rule Definitions ─────────────────────────────────────────────────
9
9
 
@@ -395,7 +395,7 @@ export function manifestoData(customDbPath, opts = {}) {
395
395
  const db = openReadonlyOrFail(customDbPath);
396
396
 
397
397
  try {
398
- const config = loadConfig(process.cwd());
398
+ const config = opts.config || loadConfig(process.cwd());
399
399
  const rules = resolveRules(config.manifesto?.rules);
400
400
 
401
401
  const violations = [];
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { findDbPath, openReadonlyOrFail } from './db.js';
4
- import { isTestFile } from './infrastructure/test-filter.js';
3
+ import { findDbPath, openReadonlyOrFail } from '../db/index.js';
4
+ import { isTestFile } from '../infrastructure/test-filter.js';
5
5
 
6
6
  // ─── CODEOWNERS Parsing ──────────────────────────────────────────────
7
7
 
@@ -6,10 +6,11 @@
6
6
  * sequence-diagram conventions.
7
7
  */
8
8
 
9
- import { findCallees, openReadonlyOrFail } from './db.js';
10
- import { isTestFile } from './infrastructure/test-filter.js';
11
- import { paginateResult } from './paginate.js';
12
- import { findMatchingNodes } from './queries.js';
9
+ import { openRepo } from '../db/index.js';
10
+ import { SqliteRepository } from '../db/repository/sqlite-repository.js';
11
+ import { findMatchingNodes } from '../domain/queries.js';
12
+ import { isTestFile } from '../infrastructure/test-filter.js';
13
+ import { paginateResult } from '../shared/paginate.js';
13
14
  import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
14
15
 
15
16
  // ─── Alias generation ────────────────────────────────────────────────
@@ -85,19 +86,19 @@ function buildAliases(files) {
85
86
  * @returns {{ entry, participants, messages, depth, totalMessages, truncated }}
86
87
  */
87
88
  export function sequenceData(name, dbPath, opts = {}) {
88
- const db = openReadonlyOrFail(dbPath);
89
+ const { repo, close } = openRepo(dbPath, opts);
89
90
  try {
90
91
  const maxDepth = opts.depth || 10;
91
92
  const noTests = opts.noTests || false;
92
93
  const withDataflow = opts.dataflow || false;
93
94
 
94
95
  // Phase 1: Direct LIKE match
95
- let matchNode = findMatchingNodes(db, name, opts)[0] ?? null;
96
+ let matchNode = findMatchingNodes(repo, name, opts)[0] ?? null;
96
97
 
97
98
  // Phase 2: Prefix-stripped matching
98
99
  if (!matchNode) {
99
100
  for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
100
- matchNode = findMatchingNodes(db, `${prefix}${name}`, opts)[0] ?? null;
101
+ matchNode = findMatchingNodes(repo, `${prefix}${name}`, opts)[0] ?? null;
101
102
  if (matchNode) break;
102
103
  }
103
104
  }
@@ -133,7 +134,7 @@ export function sequenceData(name, dbPath, opts = {}) {
133
134
  const nextFrontier = [];
134
135
 
135
136
  for (const fid of frontier) {
136
- const callees = findCallees(db, fid);
137
+ const callees = repo.findCallees(fid);
137
138
 
138
139
  const caller = idToNode.get(fid);
139
140
 
@@ -163,18 +164,17 @@ export function sequenceData(name, dbPath, opts = {}) {
163
164
 
164
165
  if (d === maxDepth && frontier.length > 0) {
165
166
  // Only mark truncated if at least one frontier node has further callees
166
- const hasMoreCalls = frontier.some((fid) => findCallees(db, fid).length > 0);
167
+ const hasMoreCalls = frontier.some((fid) => repo.findCallees(fid).length > 0);
167
168
  if (hasMoreCalls) truncated = true;
168
169
  }
169
170
  }
170
171
 
171
172
  // Dataflow annotations: add return arrows
172
173
  if (withDataflow && messages.length > 0) {
173
- const hasTable = db
174
- .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='dataflow'")
175
- .get();
174
+ const hasTable = repo.hasDataflowTable();
176
175
 
177
- if (hasTable) {
176
+ if (hasTable && repo instanceof SqliteRepository) {
177
+ const db = repo.db;
178
178
  // Build name|file lookup for O(1) target node access
179
179
  const nodeByNameFile = new Map();
180
180
  for (const n of idToNode.values()) {
@@ -281,45 +281,9 @@ export function sequenceData(name, dbPath, opts = {}) {
281
281
  }
282
282
  return result;
283
283
  } finally {
284
- db.close();
284
+ close();
285
285
  }
286
286
  }
287
287
 
288
- // ─── Mermaid formatter ───────────────────────────────────────────────
289
-
290
- /**
291
- * Escape special Mermaid characters in labels.
292
- */
293
- function escapeMermaid(str) {
294
- return str
295
- .replace(/</g, '&lt;')
296
- .replace(/>/g, '&gt;')
297
- .replace(/:/g, '#colon;')
298
- .replace(/"/g, '#quot;');
299
- }
300
-
301
- /**
302
- * Convert sequenceData result to Mermaid sequenceDiagram syntax.
303
- * @param {{ participants, messages, truncated }} seqResult
304
- * @returns {string}
305
- */
306
- export function sequenceToMermaid(seqResult) {
307
- const lines = ['sequenceDiagram'];
308
-
309
- for (const p of seqResult.participants) {
310
- lines.push(` participant ${p.id} as ${escapeMermaid(p.label)}`);
311
- }
312
-
313
- for (const msg of seqResult.messages) {
314
- const arrow = msg.type === 'return' ? '-->>' : '->>';
315
- lines.push(` ${msg.from}${arrow}${msg.to}: ${escapeMermaid(msg.label)}`);
316
- }
317
-
318
- if (seqResult.truncated && seqResult.participants.length > 0) {
319
- lines.push(
320
- ` note right of ${seqResult.participants[0].id}: Truncated at depth ${seqResult.depth}`,
321
- );
322
- }
323
-
324
- return lines.join('\n');
325
- }
288
+ // Re-export Mermaid renderer from presentation layer
289
+ export { sequenceToMermaid } from '../presentation/sequence-renderer.js';
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import Database from 'better-sqlite3';
4
- import { findDbPath } from './db.js';
5
- import { debug } from './logger.js';
4
+ import { findDbPath } from '../db/index.js';
5
+ import { debug } from '../infrastructure/logger.js';
6
+ import { ConfigError, DbError } from '../shared/errors.js';
6
7
 
7
8
  const NAME_RE = /^[a-zA-Z0-9_-]+$/;
8
9
 
@@ -12,7 +13,7 @@ const NAME_RE = /^[a-zA-Z0-9_-]+$/;
12
13
  */
13
14
  export function validateSnapshotName(name) {
14
15
  if (!name || !NAME_RE.test(name)) {
15
- throw new Error(
16
+ throw new ConfigError(
16
17
  `Invalid snapshot name "${name}". Use only letters, digits, hyphens, and underscores.`,
17
18
  );
18
19
  }
@@ -39,7 +40,7 @@ export function snapshotSave(name, options = {}) {
39
40
  validateSnapshotName(name);
40
41
  const dbPath = options.dbPath || findDbPath();
41
42
  if (!fs.existsSync(dbPath)) {
42
- throw new Error(`Database not found: ${dbPath}`);
43
+ throw new DbError(`Database not found: ${dbPath}`, { file: dbPath });
43
44
  }
44
45
 
45
46
  const dir = snapshotsDir(dbPath);
@@ -47,7 +48,7 @@ export function snapshotSave(name, options = {}) {
47
48
 
48
49
  if (fs.existsSync(dest)) {
49
50
  if (!options.force) {
50
- throw new Error(`Snapshot "${name}" already exists. Use --force to overwrite.`);
51
+ throw new ConfigError(`Snapshot "${name}" already exists. Use --force to overwrite.`);
51
52
  }
52
53
  fs.unlinkSync(dest);
53
54
  debug(`Deleted existing snapshot: ${dest}`);
@@ -82,7 +83,7 @@ export function snapshotRestore(name, options = {}) {
82
83
  const src = path.join(dir, `${name}.db`);
83
84
 
84
85
  if (!fs.existsSync(src)) {
85
- throw new Error(`Snapshot "${name}" not found at ${src}`);
86
+ throw new DbError(`Snapshot "${name}" not found at ${src}`, { file: src });
86
87
  }
87
88
 
88
89
  // Remove WAL/SHM sidecar files for a clean restore
@@ -141,7 +142,7 @@ export function snapshotDelete(name, options = {}) {
141
142
  const target = path.join(dir, `${name}.db`);
142
143
 
143
144
  if (!fs.existsSync(target)) {
144
- throw new Error(`Snapshot "${name}" not found at ${target}`);
145
+ throw new DbError(`Snapshot "${name}" not found at ${target}`, { file: target });
145
146
  }
146
147
 
147
148
  fs.unlinkSync(target);
@@ -1,9 +1,9 @@
1
1
  import path from 'node:path';
2
- import { normalizePath } from './constants.js';
3
- import { getNodeId, openReadonlyOrFail, testFilterSQL } from './db.js';
4
- import { isTestFile } from './infrastructure/test-filter.js';
5
- import { debug } from './logger.js';
6
- import { paginateResult } from './paginate.js';
2
+ import { getNodeId, openReadonlyOrFail, testFilterSQL } from '../db/index.js';
3
+ import { debug } from '../infrastructure/logger.js';
4
+ import { isTestFile } from '../infrastructure/test-filter.js';
5
+ import { normalizePath } from '../shared/constants.js';
6
+ import { paginateResult } from '../shared/paginate.js';
7
7
 
8
8
  // ─── Build-time: insert directory nodes, contains edges, and metrics ────
9
9
 
@@ -312,13 +312,10 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
312
312
 
313
313
  // ─── Node role classification ─────────────────────────────────────────
314
314
 
315
- export const FRAMEWORK_ENTRY_PREFIXES = ['route:', 'event:', 'command:'];
315
+ // Re-export from classifier for backward compatibility
316
+ export { FRAMEWORK_ENTRY_PREFIXES } from '../graph/classifiers/roles.js';
316
317
 
317
- function median(sorted) {
318
- if (sorted.length === 0) return 0;
319
- const mid = Math.floor(sorted.length / 2);
320
- return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
321
- }
318
+ import { classifyRoles } from '../graph/classifiers/roles.js';
322
319
 
323
320
  export function classifyNodeRoles(db) {
324
321
  const rows = db
@@ -354,44 +351,22 @@ export function classifyNodeRoles(db) {
354
351
  .map((r) => r.target_id),
355
352
  );
356
353
 
357
- const nonZeroFanIn = rows
358
- .filter((r) => r.fan_in > 0)
359
- .map((r) => r.fan_in)
360
- .sort((a, b) => a - b);
361
- const nonZeroFanOut = rows
362
- .filter((r) => r.fan_out > 0)
363
- .map((r) => r.fan_out)
364
- .sort((a, b) => a - b);
354
+ // Delegate classification to the pure-logic classifier
355
+ const classifierInput = rows.map((r) => ({
356
+ id: String(r.id),
357
+ name: r.name,
358
+ fanIn: r.fan_in,
359
+ fanOut: r.fan_out,
360
+ isExported: exportedIds.has(r.id),
361
+ }));
365
362
 
366
- const medFanIn = median(nonZeroFanIn);
367
- const medFanOut = median(nonZeroFanOut);
363
+ const roleMap = classifyRoles(classifierInput);
368
364
 
369
- const updates = [];
365
+ // Build summary and updates
370
366
  const summary = { entry: 0, core: 0, utility: 0, adapter: 0, dead: 0, leaf: 0 };
371
-
367
+ const updates = [];
372
368
  for (const row of rows) {
373
- const highIn = row.fan_in >= medFanIn && row.fan_in > 0;
374
- const highOut = row.fan_out >= medFanOut && row.fan_out > 0;
375
- const isExported = exportedIds.has(row.id);
376
-
377
- let role;
378
- const isFrameworkEntry = FRAMEWORK_ENTRY_PREFIXES.some((p) => row.name.startsWith(p));
379
- if (isFrameworkEntry) {
380
- role = 'entry';
381
- } else if (row.fan_in === 0 && !isExported) {
382
- role = 'dead';
383
- } else if (row.fan_in === 0 && isExported) {
384
- role = 'entry';
385
- } else if (highIn && !highOut) {
386
- role = 'core';
387
- } else if (highIn && highOut) {
388
- role = 'utility';
389
- } else if (!highIn && highOut) {
390
- role = 'adapter';
391
- } else {
392
- role = 'leaf';
393
- }
394
-
369
+ const role = roleMap.get(String(row.id)) || 'leaf';
395
370
  updates.push({ id: row.id, role });
396
371
  summary[role]++;
397
372
  }