@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
@@ -1,49 +1,25 @@
1
+ /**
2
+ * Interactive HTML viewer — presentation layer.
3
+ *
4
+ * Exports two concerns:
5
+ * - renderPlotHTML(): pure data → HTML transform (no I/O) that receives
6
+ * prepared graph data and config, returns a self-contained HTML string
7
+ * with vis-network. All graph data must be pre-loaded via prepareGraphData().
8
+ * - loadPlotConfig(): reads .plotDotCfg / .plotDotCfg.json files from disk
9
+ * and merges them with defaults. This performs filesystem I/O.
10
+ *
11
+ * Color constants are defined in ./colors.js and re-exported here for
12
+ * backward compatibility.
13
+ */
14
+
1
15
  import fs from 'node:fs';
2
16
  import path from 'node:path';
3
- import Graph from 'graphology';
4
- import louvain from 'graphology-communities-louvain';
5
- import { isTestFile } from './infrastructure/test-filter.js';
6
-
7
- const DEFAULT_MIN_CONFIDENCE = 0.5;
8
-
9
- const DEFAULT_NODE_COLORS = {
10
- function: '#4CAF50',
11
- method: '#66BB6A',
12
- class: '#2196F3',
13
- interface: '#42A5F5',
14
- type: '#7E57C2',
15
- struct: '#FF7043',
16
- enum: '#FFA726',
17
- trait: '#26A69A',
18
- record: '#EC407A',
19
- module: '#78909C',
20
- file: '#90A4AE',
21
- };
17
+ import { COMMUNITY_COLORS, DEFAULT_NODE_COLORS, DEFAULT_ROLE_COLORS } from './colors.js';
22
18
 
23
- const DEFAULT_ROLE_COLORS = {
24
- entry: '#e8f5e9',
25
- core: '#e3f2fd',
26
- utility: '#f5f5f5',
27
- dead: '#ffebee',
28
- leaf: '#fffde7',
29
- };
19
+ // Re-export color constants so existing consumers are unaffected
20
+ export { COMMUNITY_COLORS, DEFAULT_NODE_COLORS, DEFAULT_ROLE_COLORS };
30
21
 
31
- const COMMUNITY_COLORS = [
32
- '#4CAF50',
33
- '#2196F3',
34
- '#FF9800',
35
- '#9C27B0',
36
- '#F44336',
37
- '#00BCD4',
38
- '#CDDC39',
39
- '#E91E63',
40
- '#3F51B5',
41
- '#FF5722',
42
- '#009688',
43
- '#795548',
44
- ];
45
-
46
- const DEFAULT_CONFIG = {
22
+ export const DEFAULT_CONFIG = {
47
23
  layout: { algorithm: 'hierarchical', direction: 'LR' },
48
24
  physics: { enabled: true, nodeDistance: 150 },
49
25
  nodeColors: DEFAULT_NODE_COLORS,
@@ -60,6 +36,8 @@ const DEFAULT_CONFIG = {
60
36
  riskThresholds: { highBlastRadius: 10, lowMI: 40 },
61
37
  };
62
38
 
39
+ // ─── Config Loading ──────────────────────────────────────────────────
40
+
63
41
  /**
64
42
  * Load .plotDotCfg or .plotDotCfg.json from given directory.
65
43
  * Returns merged config with defaults.
@@ -105,310 +83,66 @@ export function loadPlotConfig(dir) {
105
83
  return { ...DEFAULT_CONFIG };
106
84
  }
107
85
 
108
- // ─── Data Preparation ─────────────────────────────────────────────────
109
-
110
- /**
111
- * Prepare enriched graph data for the HTML viewer.
112
- */
113
- export function prepareGraphData(db, opts = {}) {
114
- const fileLevel = opts.fileLevel !== false;
115
- const noTests = opts.noTests || false;
116
- const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
117
- const cfg = opts.config || DEFAULT_CONFIG;
118
-
119
- return fileLevel
120
- ? prepareFileLevelData(db, noTests, minConf, cfg)
121
- : prepareFunctionLevelData(db, noTests, minConf, cfg);
122
- }
123
-
124
- function prepareFunctionLevelData(db, noTests, minConf, cfg) {
125
- let edges = db
126
- .prepare(
127
- `
128
- SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
129
- n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
130
- n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
131
- n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
132
- e.kind AS edge_kind
133
- FROM edges e
134
- JOIN nodes n1 ON e.source_id = n1.id
135
- JOIN nodes n2 ON e.target_id = n2.id
136
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
137
- AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
138
- AND e.kind = 'calls'
139
- AND e.confidence >= ?
140
- `,
141
- )
142
- .all(minConf);
143
- if (noTests)
144
- edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
145
-
146
- if (cfg.filter.kinds) {
147
- const kinds = new Set(cfg.filter.kinds);
148
- edges = edges.filter((e) => kinds.has(e.source_kind) && kinds.has(e.target_kind));
149
- }
150
- if (cfg.filter.files) {
151
- const patterns = cfg.filter.files;
152
- edges = edges.filter(
153
- (e) =>
154
- patterns.some((p) => e.source_file.includes(p)) &&
155
- patterns.some((p) => e.target_file.includes(p)),
156
- );
157
- }
158
-
159
- const nodeMap = new Map();
160
- for (const e of edges) {
161
- if (!nodeMap.has(e.source_id)) {
162
- nodeMap.set(e.source_id, {
163
- id: e.source_id,
164
- name: e.source_name,
165
- kind: e.source_kind,
166
- file: e.source_file,
167
- line: e.source_line,
168
- role: e.source_role,
169
- });
170
- }
171
- if (!nodeMap.has(e.target_id)) {
172
- nodeMap.set(e.target_id, {
173
- id: e.target_id,
174
- name: e.target_name,
175
- kind: e.target_kind,
176
- file: e.target_file,
177
- line: e.target_line,
178
- role: e.target_role,
179
- });
180
- }
181
- }
182
-
183
- if (cfg.filter.roles) {
184
- const roles = new Set(cfg.filter.roles);
185
- for (const [id, n] of nodeMap) {
186
- if (!roles.has(n.role)) nodeMap.delete(id);
187
- }
188
- const nodeIds = new Set(nodeMap.keys());
189
- edges = edges.filter((e) => nodeIds.has(e.source_id) && nodeIds.has(e.target_id));
190
- }
191
-
192
- // Complexity data
193
- const complexityMap = new Map();
194
- try {
195
- const rows = db
196
- .prepare(
197
- 'SELECT node_id, cognitive, cyclomatic, max_nesting, maintainability_index FROM function_complexity',
198
- )
199
- .all();
200
- for (const r of rows) {
201
- complexityMap.set(r.node_id, {
202
- cognitive: r.cognitive,
203
- cyclomatic: r.cyclomatic,
204
- maintainabilityIndex: r.maintainability_index,
205
- });
206
- }
207
- } catch {
208
- // table may not exist in old DBs
209
- }
210
-
211
- // Fan-in / fan-out
212
- const fanInMap = new Map();
213
- const fanOutMap = new Map();
214
- const fanInRows = db
215
- .prepare(
216
- "SELECT target_id AS node_id, COUNT(*) AS fan_in FROM edges WHERE kind = 'calls' GROUP BY target_id",
217
- )
218
- .all();
219
- for (const r of fanInRows) fanInMap.set(r.node_id, r.fan_in);
220
-
221
- const fanOutRows = db
222
- .prepare(
223
- "SELECT source_id AS node_id, COUNT(*) AS fan_out FROM edges WHERE kind = 'calls' GROUP BY source_id",
224
- )
225
- .all();
226
- for (const r of fanOutRows) fanOutMap.set(r.node_id, r.fan_out);
227
-
228
- // Communities (Louvain)
229
- const communityMap = new Map();
230
- if (nodeMap.size > 0) {
231
- try {
232
- const graph = new Graph({ type: 'undirected' });
233
- for (const [id] of nodeMap) graph.addNode(String(id));
234
- for (const e of edges) {
235
- const src = String(e.source_id);
236
- const tgt = String(e.target_id);
237
- if (src !== tgt && !graph.hasEdge(src, tgt)) graph.addEdge(src, tgt);
238
- }
239
- const communities = louvain(graph);
240
- for (const [nid, cid] of Object.entries(communities)) communityMap.set(Number(nid), cid);
241
- } catch {
242
- // louvain can fail on disconnected graphs
243
- }
244
- }
245
-
246
- // Build enriched nodes
247
- const visNodes = [...nodeMap.values()].map((n) => {
248
- const cx = complexityMap.get(n.id) || null;
249
- const fanIn = fanInMap.get(n.id) || 0;
250
- const fanOut = fanOutMap.get(n.id) || 0;
251
- const community = communityMap.get(n.id) ?? null;
252
- const directory = path.dirname(n.file);
253
- const risk = [];
254
- if (n.role === 'dead') risk.push('dead-code');
255
- if (fanIn >= (cfg.riskThresholds?.highBlastRadius ?? 10)) risk.push('high-blast-radius');
256
- if (cx && cx.maintainabilityIndex < (cfg.riskThresholds?.lowMI ?? 40)) risk.push('low-mi');
257
-
258
- const color =
259
- cfg.colorBy === 'role' && n.role
260
- ? cfg.roleColors[n.role] || DEFAULT_ROLE_COLORS[n.role] || '#ccc'
261
- : cfg.colorBy === 'community' && community !== null
262
- ? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length]
263
- : cfg.nodeColors[n.kind] || DEFAULT_NODE_COLORS[n.kind] || '#ccc';
264
-
265
- return {
266
- id: n.id,
267
- label: n.name,
268
- title: `${n.file}:${n.line} (${n.kind}${n.role ? `, ${n.role}` : ''})`,
269
- color,
270
- kind: n.kind,
271
- role: n.role || '',
272
- file: n.file,
273
- line: n.line,
274
- community,
275
- cognitive: cx?.cognitive ?? null,
276
- cyclomatic: cx?.cyclomatic ?? null,
277
- maintainabilityIndex: cx?.maintainabilityIndex ?? null,
278
- fanIn,
279
- fanOut,
280
- directory,
281
- risk,
282
- };
283
- });
284
-
285
- const visEdges = edges.map((e, i) => ({
286
- id: `e${i}`,
287
- from: e.source_id,
288
- to: e.target_id,
289
- }));
290
-
291
- // Seed strategy
292
- let seedNodeIds;
293
- if (cfg.seedStrategy === 'top-fanin') {
294
- const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
295
- seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
296
- } else if (cfg.seedStrategy === 'entry') {
297
- seedNodeIds = visNodes.filter((n) => n.role === 'entry').map((n) => n.id);
298
- } else {
299
- seedNodeIds = visNodes.map((n) => n.id);
300
- }
86
+ // ─── Internal Helpers ────────────────────────────────────────────────
301
87
 
302
- return { nodes: visNodes, edges: visEdges, seedNodeIds };
88
+ export function escapeHtml(s) {
89
+ return String(s)
90
+ .replace(/&/g, '&amp;')
91
+ .replace(/</g, '&lt;')
92
+ .replace(/>/g, '&gt;')
93
+ .replace(/"/g, '&quot;');
303
94
  }
304
95
 
305
- function prepareFileLevelData(db, noTests, minConf, cfg) {
306
- let edges = db
307
- .prepare(
308
- `
309
- SELECT DISTINCT n1.file AS source, n2.file AS target
310
- FROM edges e
311
- JOIN nodes n1 ON e.source_id = n1.id
312
- JOIN nodes n2 ON e.target_id = n2.id
313
- WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
314
- AND e.confidence >= ?
315
- `,
316
- )
317
- .all(minConf);
318
- if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
319
-
320
- const files = new Set();
321
- for (const { source, target } of edges) {
322
- files.add(source);
323
- files.add(target);
324
- }
325
-
326
- const fileIds = new Map();
327
- let idx = 0;
328
- for (const f of files) fileIds.set(f, idx++);
329
-
330
- // Fan-in/fan-out
331
- const fanInCount = new Map();
332
- const fanOutCount = new Map();
333
- for (const { source, target } of edges) {
334
- fanOutCount.set(source, (fanOutCount.get(source) || 0) + 1);
335
- fanInCount.set(target, (fanInCount.get(target) || 0) + 1);
336
- }
337
-
338
- // Communities
339
- const communityMap = new Map();
340
- if (files.size > 0) {
341
- try {
342
- const graph = new Graph({ type: 'undirected' });
343
- for (const f of files) graph.addNode(f);
344
- for (const { source, target } of edges) {
345
- if (source !== target && !graph.hasEdge(source, target)) graph.addEdge(source, target);
346
- }
347
- const communities = louvain(graph);
348
- for (const [file, cid] of Object.entries(communities)) communityMap.set(file, cid);
349
- } catch {
350
- // ignore
351
- }
352
- }
96
+ export function buildLayoutOptions(cfg) {
97
+ const opts = {
98
+ nodes: {
99
+ shape: 'box',
100
+ font: { face: 'monospace', size: 12 },
101
+ },
102
+ edges: {
103
+ arrows: 'to',
104
+ color: cfg.edgeStyle.color || '#666',
105
+ smooth: cfg.edgeStyle.smooth !== false,
106
+ },
107
+ physics: {
108
+ enabled: cfg.physics.enabled !== false,
109
+ barnesHut: {
110
+ gravitationalConstant: -3000,
111
+ springLength: cfg.physics.nodeDistance || 150,
112
+ },
113
+ },
114
+ interaction: {
115
+ tooltipDelay: 200,
116
+ hover: true,
117
+ },
118
+ };
353
119
 
354
- const visNodes = [...files].map((f) => {
355
- const id = fileIds.get(f);
356
- const community = communityMap.get(f) ?? null;
357
- const fanIn = fanInCount.get(f) || 0;
358
- const fanOut = fanOutCount.get(f) || 0;
359
- const directory = path.dirname(f);
360
- const color =
361
- cfg.colorBy === 'community' && community !== null
362
- ? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length]
363
- : cfg.nodeColors.file || DEFAULT_NODE_COLORS.file;
364
-
365
- return {
366
- id,
367
- label: path.basename(f),
368
- title: f,
369
- color,
370
- kind: 'file',
371
- role: '',
372
- file: f,
373
- line: 0,
374
- community,
375
- cognitive: null,
376
- cyclomatic: null,
377
- maintainabilityIndex: null,
378
- fanIn,
379
- fanOut,
380
- directory,
381
- risk: [],
120
+ if (cfg.layout.algorithm === 'hierarchical') {
121
+ opts.layout = {
122
+ hierarchical: {
123
+ enabled: true,
124
+ direction: cfg.layout.direction || 'LR',
125
+ sortMethod: 'directed',
126
+ nodeSpacing: cfg.physics.nodeDistance || 150,
127
+ },
382
128
  };
383
- });
384
-
385
- const visEdges = edges.map(({ source, target }, i) => ({
386
- id: `e${i}`,
387
- from: fileIds.get(source),
388
- to: fileIds.get(target),
389
- }));
390
-
391
- let seedNodeIds;
392
- if (cfg.seedStrategy === 'top-fanin') {
393
- const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
394
- seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
395
- } else if (cfg.seedStrategy === 'entry') {
396
- seedNodeIds = visNodes.map((n) => n.id);
397
- } else {
398
- seedNodeIds = visNodes.map((n) => n.id);
399
129
  }
400
130
 
401
- return { nodes: visNodes, edges: visEdges, seedNodeIds };
131
+ return opts;
402
132
  }
403
133
 
404
- // ─── HTML Generation ──────────────────────────────────────────────────
134
+ // ─── HTML Renderer ───────────────────────────────────────────────────
405
135
 
406
136
  /**
407
- * Generate a self-contained interactive HTML file with vis-network.
137
+ * Render a self-contained interactive HTML file with vis-network.
138
+ *
139
+ * Pure transform: prepared graph data + config → HTML string.
140
+ *
141
+ * @param {{ nodes: Array, edges: Array, seedNodeIds: Array }} data - From prepareGraphData()
142
+ * @param {object} cfg - Viewer config (from loadPlotConfig or DEFAULT_CONFIG)
143
+ * @returns {string} Complete HTML document
408
144
  */
409
- export function generatePlotHTML(db, opts = {}) {
410
- const cfg = opts.config || DEFAULT_CONFIG;
411
- const data = prepareGraphData(db, opts);
145
+ export function renderPlotHTML(data, cfg) {
412
146
  const layoutOpts = buildLayoutOptions(cfg);
413
147
  const title = cfg.title || 'Codegraph';
414
148
 
@@ -898,51 +632,3 @@ ${(cfg.clusterBy || 'none') !== 'none' ? `applyClusterBy(${JSON.stringify(cfg.cl
898
632
  </body>
899
633
  </html>`;
900
634
  }
901
-
902
- // ─── Internal Helpers ─────────────────────────────────────────────────
903
-
904
- function escapeHtml(s) {
905
- return String(s)
906
- .replace(/&/g, '&amp;')
907
- .replace(/</g, '&lt;')
908
- .replace(/>/g, '&gt;')
909
- .replace(/"/g, '&quot;');
910
- }
911
-
912
- function buildLayoutOptions(cfg) {
913
- const opts = {
914
- nodes: {
915
- shape: 'box',
916
- font: { face: 'monospace', size: 12 },
917
- },
918
- edges: {
919
- arrows: 'to',
920
- color: cfg.edgeStyle.color || '#666',
921
- smooth: cfg.edgeStyle.smooth !== false,
922
- },
923
- physics: {
924
- enabled: cfg.physics.enabled !== false,
925
- barnesHut: {
926
- gravitationalConstant: -3000,
927
- springLength: cfg.physics.nodeDistance || 150,
928
- },
929
- },
930
- interaction: {
931
- tooltipDelay: 200,
932
- hover: true,
933
- },
934
- };
935
-
936
- if (cfg.layout.algorithm === 'hierarchical') {
937
- opts.layout = {
938
- hierarchical: {
939
- enabled: true,
940
- direction: cfg.layout.direction || 'LR',
941
- sortMethod: 'directed',
942
- nodeSpacing: cfg.physics.nodeDistance || 150,
943
- },
944
- };
945
- }
946
-
947
- return opts;
948
- }
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { SUPPORTED_EXTENSIONS } from './parser.js';
2
+ import { SUPPORTED_EXTENSIONS } from '../domain/parser.js';
3
3
 
4
4
  export const IGNORE_DIRS = new Set([
5
5
  'node_modules',
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Domain error hierarchy for codegraph.
3
+ *
4
+ * Library code throws these instead of calling process.exit() or throwing
5
+ * bare Error instances. The CLI top-level catch formats them for humans;
6
+ * MCP returns structured { isError, code } responses.
7
+ */
8
+
9
+ export class CodegraphError extends Error {
10
+ /** @type {string} */
11
+ code;
12
+
13
+ /** @type {string|undefined} */
14
+ file;
15
+
16
+ /**
17
+ * @param {string} message
18
+ * @param {object} [opts]
19
+ * @param {string} [opts.code]
20
+ * @param {string} [opts.file] - Related file path, if applicable
21
+ * @param {Error} [opts.cause] - Original error that triggered this one
22
+ */
23
+ constructor(message, { code = 'CODEGRAPH_ERROR', file, cause } = {}) {
24
+ super(message, { cause });
25
+ this.name = 'CodegraphError';
26
+ this.code = code;
27
+ this.file = file;
28
+ }
29
+ }
30
+
31
+ export class ParseError extends CodegraphError {
32
+ constructor(message, opts = {}) {
33
+ super(message, { code: 'PARSE_FAILED', ...opts });
34
+ this.name = 'ParseError';
35
+ }
36
+ }
37
+
38
+ export class DbError extends CodegraphError {
39
+ constructor(message, opts = {}) {
40
+ super(message, { code: 'DB_ERROR', ...opts });
41
+ this.name = 'DbError';
42
+ }
43
+ }
44
+
45
+ export class ConfigError extends CodegraphError {
46
+ constructor(message, opts = {}) {
47
+ super(message, { code: 'CONFIG_INVALID', ...opts });
48
+ this.name = 'ConfigError';
49
+ }
50
+ }
51
+
52
+ export class ResolutionError extends CodegraphError {
53
+ constructor(message, opts = {}) {
54
+ super(message, { code: 'RESOLUTION_FAILED', ...opts });
55
+ this.name = 'ResolutionError';
56
+ }
57
+ }
58
+
59
+ export class EngineError extends CodegraphError {
60
+ constructor(message, opts = {}) {
61
+ super(message, { code: 'ENGINE_UNAVAILABLE', ...opts });
62
+ this.name = 'EngineError';
63
+ }
64
+ }
65
+
66
+ export class AnalysisError extends CodegraphError {
67
+ constructor(message, opts = {}) {
68
+ super(message, { code: 'ANALYSIS_FAILED', ...opts });
69
+ this.name = 'AnalysisError';
70
+ }
71
+ }
72
+
73
+ export class BoundaryError extends CodegraphError {
74
+ constructor(message, opts = {}) {
75
+ super(message, { code: 'BOUNDARY_VIOLATION', ...opts });
76
+ this.name = 'BoundaryError';
77
+ }
78
+ }