@optave/codegraph 3.1.2 → 3.1.4

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 (194) hide show
  1. package/README.md +19 -21
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1472
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1514
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/complexity.js +1 -1
  76. package/src/cycles.js +30 -85
  77. package/src/dataflow.js +1 -2
  78. package/src/db/connection.js +4 -4
  79. package/src/db/migrations.js +41 -0
  80. package/src/db/query-builder.js +6 -5
  81. package/src/db/repository/base.js +201 -0
  82. package/src/db/repository/cached-stmt.js +19 -0
  83. package/src/db/repository/cfg.js +27 -38
  84. package/src/db/repository/cochange.js +16 -3
  85. package/src/db/repository/complexity.js +11 -6
  86. package/src/db/repository/dataflow.js +6 -1
  87. package/src/db/repository/edges.js +120 -98
  88. package/src/db/repository/embeddings.js +14 -3
  89. package/src/db/repository/graph-read.js +32 -9
  90. package/src/db/repository/in-memory-repository.js +584 -0
  91. package/src/db/repository/index.js +6 -1
  92. package/src/db/repository/nodes.js +110 -40
  93. package/src/db/repository/sqlite-repository.js +219 -0
  94. package/src/db.js +5 -0
  95. package/src/embeddings/generator.js +163 -0
  96. package/src/embeddings/index.js +13 -0
  97. package/src/embeddings/models.js +218 -0
  98. package/src/embeddings/search/cli-formatter.js +151 -0
  99. package/src/embeddings/search/filters.js +46 -0
  100. package/src/embeddings/search/hybrid.js +121 -0
  101. package/src/embeddings/search/keyword.js +68 -0
  102. package/src/embeddings/search/prepare.js +66 -0
  103. package/src/embeddings/search/semantic.js +145 -0
  104. package/src/embeddings/stores/fts5.js +27 -0
  105. package/src/embeddings/stores/sqlite-blob.js +24 -0
  106. package/src/embeddings/strategies/source.js +14 -0
  107. package/src/embeddings/strategies/structured.js +43 -0
  108. package/src/embeddings/strategies/text-utils.js +43 -0
  109. package/src/errors.js +78 -0
  110. package/src/export.js +217 -520
  111. package/src/extractors/csharp.js +10 -2
  112. package/src/extractors/go.js +3 -1
  113. package/src/extractors/helpers.js +71 -0
  114. package/src/extractors/java.js +9 -2
  115. package/src/extractors/javascript.js +38 -1
  116. package/src/extractors/php.js +3 -1
  117. package/src/extractors/python.js +14 -3
  118. package/src/extractors/rust.js +3 -1
  119. package/src/graph/algorithms/bfs.js +49 -0
  120. package/src/graph/algorithms/centrality.js +16 -0
  121. package/src/graph/algorithms/index.js +5 -0
  122. package/src/graph/algorithms/louvain.js +26 -0
  123. package/src/graph/algorithms/shortest-path.js +41 -0
  124. package/src/graph/algorithms/tarjan.js +49 -0
  125. package/src/graph/builders/dependency.js +91 -0
  126. package/src/graph/builders/index.js +3 -0
  127. package/src/graph/builders/structure.js +40 -0
  128. package/src/graph/builders/temporal.js +33 -0
  129. package/src/graph/classifiers/index.js +2 -0
  130. package/src/graph/classifiers/risk.js +85 -0
  131. package/src/graph/classifiers/roles.js +64 -0
  132. package/src/graph/index.js +13 -0
  133. package/src/graph/model.js +230 -0
  134. package/src/index.js +33 -204
  135. package/src/infrastructure/result-formatter.js +2 -21
  136. package/src/mcp/index.js +2 -0
  137. package/src/mcp/middleware.js +26 -0
  138. package/src/mcp/server.js +128 -0
  139. package/src/mcp/tool-registry.js +801 -0
  140. package/src/mcp/tools/ast-query.js +14 -0
  141. package/src/mcp/tools/audit.js +21 -0
  142. package/src/mcp/tools/batch-query.js +11 -0
  143. package/src/mcp/tools/branch-compare.js +10 -0
  144. package/src/mcp/tools/cfg.js +21 -0
  145. package/src/mcp/tools/check.js +43 -0
  146. package/src/mcp/tools/co-changes.js +20 -0
  147. package/src/mcp/tools/code-owners.js +12 -0
  148. package/src/mcp/tools/communities.js +15 -0
  149. package/src/mcp/tools/complexity.js +18 -0
  150. package/src/mcp/tools/context.js +17 -0
  151. package/src/mcp/tools/dataflow.js +26 -0
  152. package/src/mcp/tools/diff-impact.js +24 -0
  153. package/src/mcp/tools/execution-flow.js +26 -0
  154. package/src/mcp/tools/export-graph.js +57 -0
  155. package/src/mcp/tools/file-deps.js +12 -0
  156. package/src/mcp/tools/file-exports.js +13 -0
  157. package/src/mcp/tools/find-cycles.js +15 -0
  158. package/src/mcp/tools/fn-impact.js +15 -0
  159. package/src/mcp/tools/impact-analysis.js +12 -0
  160. package/src/mcp/tools/index.js +71 -0
  161. package/src/mcp/tools/list-functions.js +14 -0
  162. package/src/mcp/tools/list-repos.js +11 -0
  163. package/src/mcp/tools/module-map.js +6 -0
  164. package/src/mcp/tools/node-roles.js +14 -0
  165. package/src/mcp/tools/path.js +12 -0
  166. package/src/mcp/tools/query.js +30 -0
  167. package/src/mcp/tools/semantic-search.js +65 -0
  168. package/src/mcp/tools/sequence.js +17 -0
  169. package/src/mcp/tools/structure.js +15 -0
  170. package/src/mcp/tools/symbol-children.js +14 -0
  171. package/src/mcp/tools/triage.js +35 -0
  172. package/src/mcp/tools/where.js +13 -0
  173. package/src/mcp.js +2 -1470
  174. package/src/native.js +34 -10
  175. package/src/parser.js +53 -2
  176. package/src/presentation/colors.js +44 -0
  177. package/src/presentation/export.js +444 -0
  178. package/src/presentation/result-formatter.js +21 -0
  179. package/src/presentation/sequence-renderer.js +43 -0
  180. package/src/presentation/table.js +47 -0
  181. package/src/presentation/viewer.js +634 -0
  182. package/src/queries.js +35 -2276
  183. package/src/resolve.js +1 -1
  184. package/src/sequence.js +2 -38
  185. package/src/shared/file-utils.js +153 -0
  186. package/src/shared/generators.js +125 -0
  187. package/src/shared/hierarchy.js +27 -0
  188. package/src/shared/normalize.js +59 -0
  189. package/src/snapshot.js +6 -5
  190. package/src/structure.js +15 -40
  191. package/src/triage.js +20 -72
  192. package/src/viewer.js +35 -656
  193. package/src/watcher.js +8 -148
  194. package/src/embedder.js +0 -1097
package/src/viewer.js CHANGED
@@ -1,109 +1,18 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
3
- import Graph from 'graphology';
4
- import louvain from 'graphology-communities-louvain';
2
+ import { louvainCommunities } from './graph/algorithms/louvain.js';
3
+ import { CodeGraph } from './graph/model.js';
5
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';
6
11
 
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
- };
22
-
23
- const DEFAULT_ROLE_COLORS = {
24
- entry: '#e8f5e9',
25
- core: '#e3f2fd',
26
- utility: '#f5f5f5',
27
- dead: '#ffebee',
28
- leaf: '#fffde7',
29
- };
30
-
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 = {
47
- layout: { algorithm: 'hierarchical', direction: 'LR' },
48
- physics: { enabled: true, nodeDistance: 150 },
49
- nodeColors: DEFAULT_NODE_COLORS,
50
- roleColors: DEFAULT_ROLE_COLORS,
51
- colorBy: 'kind',
52
- edgeStyle: { color: '#666', smooth: true },
53
- filter: { kinds: null, roles: null, files: null },
54
- title: 'Codegraph',
55
- seedStrategy: 'all',
56
- seedCount: 30,
57
- clusterBy: 'none',
58
- sizeBy: 'uniform',
59
- overlays: { complexity: false, risk: false },
60
- riskThresholds: { highBlastRadius: 10, lowMI: 40 },
61
- };
12
+ // Re-export presentation utilities for backward compatibility
13
+ export { loadPlotConfig } from './presentation/viewer.js';
62
14
 
63
- /**
64
- * Load .plotDotCfg or .plotDotCfg.json from given directory.
65
- * Returns merged config with defaults.
66
- */
67
- export function loadPlotConfig(dir) {
68
- for (const name of ['.plotDotCfg', '.plotDotCfg.json']) {
69
- const p = path.join(dir, name);
70
- if (fs.existsSync(p)) {
71
- try {
72
- const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
73
- return {
74
- ...DEFAULT_CONFIG,
75
- ...raw,
76
- layout: { ...DEFAULT_CONFIG.layout, ...(raw.layout || {}) },
77
- physics: { ...DEFAULT_CONFIG.physics, ...(raw.physics || {}) },
78
- nodeColors: {
79
- ...DEFAULT_CONFIG.nodeColors,
80
- ...(raw.nodeColors || {}),
81
- },
82
- roleColors: {
83
- ...DEFAULT_CONFIG.roleColors,
84
- ...(raw.roleColors || {}),
85
- },
86
- edgeStyle: {
87
- ...DEFAULT_CONFIG.edgeStyle,
88
- ...(raw.edgeStyle || {}),
89
- },
90
- filter: { ...DEFAULT_CONFIG.filter, ...(raw.filter || {}) },
91
- overlays: {
92
- ...DEFAULT_CONFIG.overlays,
93
- ...(raw.overlays || {}),
94
- },
95
- riskThresholds: {
96
- ...DEFAULT_CONFIG.riskThresholds,
97
- ...(raw.riskThresholds || {}),
98
- },
99
- };
100
- } catch {
101
- // Invalid JSON — use defaults
102
- }
103
- }
104
- }
105
- return { ...DEFAULT_CONFIG };
106
- }
15
+ const DEFAULT_MIN_CONFIDENCE = 0.5;
107
16
 
108
17
  // ─── Data Preparation ─────────────────────────────────────────────────
109
18
 
@@ -208,7 +117,16 @@ function prepareFunctionLevelData(db, noTests, minConf, cfg) {
208
117
  // table may not exist in old DBs
209
118
  }
210
119
 
211
- // Fan-in / fan-out
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)
212
130
  const fanInMap = new Map();
213
131
  const fanOutMap = new Map();
214
132
  const fanInRows = db
@@ -225,19 +143,12 @@ function prepareFunctionLevelData(db, noTests, minConf, cfg) {
225
143
  .all();
226
144
  for (const r of fanOutRows) fanOutMap.set(r.node_id, r.fan_out);
227
145
 
228
- // Communities (Louvain)
146
+ // Communities (Louvain) via graph subsystem
229
147
  const communityMap = new Map();
230
148
  if (nodeMap.size > 0) {
231
149
  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);
150
+ const { assignments } = louvainCommunities(fnGraph);
151
+ for (const [nid, cid] of assignments) communityMap.set(Number(nid), cid);
241
152
  } catch {
242
153
  // louvain can fail on disconnected graphs
243
154
  }
@@ -335,17 +246,18 @@ function prepareFileLevelData(db, noTests, minConf, cfg) {
335
246
  fanInCount.set(target, (fanInCount.get(target) || 0) + 1);
336
247
  }
337
248
 
338
- // Communities
249
+ // Communities via graph subsystem
339
250
  const communityMap = new Map();
340
251
  if (files.size > 0) {
341
252
  try {
342
- const graph = new Graph({ type: 'undirected' });
343
- for (const f of files) graph.addNode(f);
253
+ const fileGraph = new CodeGraph();
254
+ for (const f of files) fileGraph.addNode(f);
344
255
  for (const { source, target } of edges) {
345
- if (source !== target && !graph.hasEdge(source, target)) graph.addEdge(source, target);
256
+ if (source !== target && !fileGraph.hasEdge(source, target))
257
+ fileGraph.addEdge(source, target);
346
258
  }
347
- const communities = louvain(graph);
348
- for (const [file, cid] of Object.entries(communities)) communityMap.set(file, cid);
259
+ const { assignments } = louvainCommunities(fileGraph);
260
+ for (const [file, cid] of assignments) communityMap.set(file, cid);
349
261
  } catch {
350
262
  // ignore
351
263
  }
@@ -401,548 +313,15 @@ function prepareFileLevelData(db, noTests, minConf, cfg) {
401
313
  return { nodes: visNodes, edges: visEdges, seedNodeIds };
402
314
  }
403
315
 
404
- // ─── HTML Generation ──────────────────────────────────────────────────
316
+ // ─── HTML Generation (thin wrapper) ──────────────────────────────────
405
317
 
406
318
  /**
407
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.
408
322
  */
409
323
  export function generatePlotHTML(db, opts = {}) {
410
324
  const cfg = opts.config || DEFAULT_CONFIG;
411
325
  const data = prepareGraphData(db, opts);
412
- const layoutOpts = buildLayoutOptions(cfg);
413
- const title = cfg.title || 'Codegraph';
414
-
415
- // Resolve effective colorBy (overlays.complexity overrides)
416
- const effectiveColorBy =
417
- cfg.overlays?.complexity && cfg.colorBy === 'kind' ? 'complexity' : cfg.colorBy || 'kind';
418
- const effectiveRisk = cfg.overlays?.risk || false;
419
-
420
- return `<!DOCTYPE html>
421
- <html lang="en">
422
- <head>
423
- <meta charset="UTF-8">
424
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
425
- <title>${escapeHtml(title)}</title>
426
- <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
427
- <style>
428
- * { margin: 0; padding: 0; box-sizing: border-box; }
429
- body { font-family: monospace; background: #fafafa; }
430
- #controls { padding: 8px 12px; background: #fff; border-bottom: 1px solid #ddd; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
431
- #controls label { font-size: 13px; }
432
- #controls select, #controls input[type="text"] { font-size: 13px; padding: 2px 6px; }
433
- #main { display: flex; height: calc(100vh - 44px); }
434
- #graph { flex: 1; }
435
- #detail { width: 320px; border-left: 1px solid #ddd; background: #fff; overflow-y: auto; display: none; padding: 12px; font-size: 13px; }
436
- #detail h3 { margin-bottom: 6px; word-break: break-all; }
437
- #detailClose { float: right; cursor: pointer; font-size: 18px; color: #999; line-height: 1; }
438
- #detailClose:hover { color: #333; }
439
- .detail-meta { margin-bottom: 4px; }
440
- .detail-file { color: #666; margin-bottom: 10px; font-size: 12px; }
441
- .detail-section { margin-bottom: 10px; }
442
- .detail-section table { width: 100%; border-collapse: collapse; }
443
- .detail-section td { padding: 2px 8px 2px 0; }
444
- .detail-section ul { list-style: none; padding: 0; }
445
- .detail-section li { padding: 2px 0; }
446
- .detail-section a { color: #1976D2; text-decoration: none; cursor: pointer; }
447
- .detail-section a:hover { text-decoration: underline; }
448
- .badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; margin-right: 4px; }
449
- .kind-badge { background: #E3F2FD; color: #1565C0; }
450
- .role-badge { background: #E8F5E9; color: #2E7D32; }
451
- .risk-badge { background: #FFEBEE; color: #C62828; }
452
- #legend { position: absolute; bottom: 12px; right: 12px; background: rgba(255,255,255,0.95); border: 1px solid #ddd; border-radius: 4px; padding: 8px 12px; font-size: 12px; max-height: 300px; overflow-y: auto; }
453
- #legend div { display: flex; align-items: center; gap: 6px; margin: 2px 0; }
454
- #legend span.swatch { width: 14px; height: 14px; border-radius: 3px; display: inline-block; flex-shrink: 0; }
455
- </style>
456
- </head>
457
- <body>
458
- <div id="controls">
459
- <label>Layout:
460
- <select id="layoutSelect">
461
- <option value="hierarchical"${cfg.layout.algorithm === 'hierarchical' ? ' selected' : ''}>Hierarchical</option>
462
- <option value="force"${cfg.layout.algorithm === 'force' ? ' selected' : ''}>Force</option>
463
- <option value="radial"${cfg.layout.algorithm === 'radial' ? ' selected' : ''}>Radial</option>
464
- </select>
465
- </label>
466
- <label>Physics: <input type="checkbox" id="physicsToggle"${cfg.physics.enabled ? ' checked' : ''}></label>
467
- <label>Search: <input type="text" id="searchInput" placeholder="Filter nodes..."></label>
468
- <label>Color by:
469
- <select id="colorBySelect">
470
- <option value="kind"${effectiveColorBy === 'kind' ? ' selected' : ''}>Kind</option>
471
- <option value="role"${effectiveColorBy === 'role' ? ' selected' : ''}>Role</option>
472
- <option value="community"${effectiveColorBy === 'community' ? ' selected' : ''}>Community</option>
473
- <option value="complexity"${effectiveColorBy === 'complexity' ? ' selected' : ''}>Complexity</option>
474
- </select>
475
- </label>
476
- <label>Size by:
477
- <select id="sizeBySelect">
478
- <option value="uniform"${(cfg.sizeBy || 'uniform') === 'uniform' ? ' selected' : ''}>Uniform</option>
479
- <option value="fan-in"${cfg.sizeBy === 'fan-in' ? ' selected' : ''}>Fan-in</option>
480
- <option value="fan-out"${cfg.sizeBy === 'fan-out' ? ' selected' : ''}>Fan-out</option>
481
- <option value="complexity"${cfg.sizeBy === 'complexity' ? ' selected' : ''}>Complexity</option>
482
- </select>
483
- </label>
484
- <label>Cluster by:
485
- <select id="clusterBySelect">
486
- <option value="none"${(cfg.clusterBy || 'none') === 'none' ? ' selected' : ''}>None</option>
487
- <option value="community"${cfg.clusterBy === 'community' ? ' selected' : ''}>Community</option>
488
- <option value="directory"${cfg.clusterBy === 'directory' ? ' selected' : ''}>Directory</option>
489
- </select>
490
- </label>
491
- <label>Risk: <input type="checkbox" id="riskToggle"${effectiveRisk ? ' checked' : ''}></label>
492
- </div>
493
- <div id="main">
494
- <div id="graph"></div>
495
- <div id="detail">
496
- <span id="detailClose">&times;</span>
497
- <div id="detailContent"></div>
498
- </div>
499
- </div>
500
- <div id="legend"></div>
501
- <script>
502
- /* ── Data ──────────────────────────────────────────────────────────── */
503
- var allNodes = ${JSON.stringify(data.nodes)};
504
- var allEdges = ${JSON.stringify(data.edges)};
505
- var seedNodeIds = ${JSON.stringify(data.seedNodeIds)};
506
- var nodeColorMap = ${JSON.stringify(cfg.nodeColors || DEFAULT_NODE_COLORS)};
507
- var roleColorMap = ${JSON.stringify(cfg.roleColors || DEFAULT_ROLE_COLORS)};
508
- var communityColors = ${JSON.stringify(COMMUNITY_COLORS)};
509
-
510
- /* ── Lookups ───────────────────────────────────────────────────────── */
511
- var nodeById = {};
512
- allNodes.forEach(function(n) { nodeById[n.id] = n; });
513
- var adjIndex = {};
514
- allNodes.forEach(function(n) { adjIndex[n.id] = { callers: [], callees: [] }; });
515
- allEdges.forEach(function(e) {
516
- if (adjIndex[e.from]) adjIndex[e.from].callees.push(e.to);
517
- if (adjIndex[e.to]) adjIndex[e.to].callers.push(e.from);
518
- });
519
-
520
- /* ── State ─────────────────────────────────────────────────────────── */
521
- var seedSet = new Set(seedNodeIds);
522
- var visibleNodeIds = new Set(seedNodeIds);
523
- var expandedNodes = new Set();
524
- var drillDownActive = ${JSON.stringify((cfg.seedStrategy || 'all') !== 'all')};
525
-
526
- /* ── Helpers ───────────────────────────────────────────────────────── */
527
- function escHtml(s) {
528
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
529
- }
530
-
531
- /* ── vis-network init ──────────────────────────────────────────────── */
532
- function getVisibleNodes() {
533
- return allNodes.filter(function(n) { return visibleNodeIds.has(n.id); });
534
- }
535
- function getVisibleEdges() {
536
- return allEdges.filter(function(e) { return visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to); });
537
- }
538
-
539
- var nodes = new vis.DataSet(getVisibleNodes());
540
- var edges = new vis.DataSet(getVisibleEdges());
541
- var container = document.getElementById('graph');
542
- var options = ${JSON.stringify(layoutOpts, null, 2)};
543
- var network = new vis.Network(container, { nodes: nodes, edges: edges }, options);
544
-
545
- /* ── Appearance ────────────────────────────────────────────────────── */
546
- function refreshNodeAppearance() {
547
- var colorBy = document.getElementById('colorBySelect').value;
548
- var sizeBy = document.getElementById('sizeBySelect').value;
549
- var riskEnabled = document.getElementById('riskToggle').checked;
550
- var updates = [];
551
-
552
- allNodes.forEach(function(n) {
553
- if (!visibleNodeIds.has(n.id)) return;
554
- var update = { id: n.id };
555
-
556
- // Background color
557
- var bg;
558
- if (colorBy === 'role') {
559
- bg = n.role ? (roleColorMap[n.role] || nodeColorMap[n.kind] || '#ccc') : (nodeColorMap[n.kind] || '#ccc');
560
- } else if (colorBy === 'community') {
561
- bg = n.community !== null ? communityColors[n.community % communityColors.length] : '#ccc';
562
- } else {
563
- bg = nodeColorMap[n.kind] || '#ccc';
564
- }
565
-
566
- var borderColor = '#888';
567
- var borderWidth = 1;
568
- var borderDashes = false;
569
- var shadow = false;
570
-
571
- // Complexity border (when colorBy is 'complexity')
572
- if (colorBy === 'complexity' && n.maintainabilityIndex !== null) {
573
- var mi = n.maintainabilityIndex;
574
- if (mi >= 80) { borderColor = '#4CAF50'; borderWidth = 2; }
575
- else if (mi >= 65) { borderColor = '#FFC107'; borderWidth = 3; }
576
- else if (mi >= 40) { borderColor = '#FF9800'; borderWidth = 3; }
577
- else { borderColor = '#F44336'; borderWidth = 4; }
578
- }
579
-
580
- // Risk overlay (overrides border when active)
581
- if (riskEnabled && n.risk && n.risk.length > 0) {
582
- if (n.risk.indexOf('dead-code') >= 0) {
583
- borderColor = '#F44336'; borderDashes = [5, 5]; borderWidth = 3;
584
- }
585
- if (n.risk.indexOf('high-blast-radius') >= 0) {
586
- borderColor = '#FF9800'; shadow = true; borderWidth = 3;
587
- }
588
- if (n.risk.indexOf('low-mi') >= 0) {
589
- borderColor = '#FF9800'; borderWidth = 3;
590
- }
591
- }
592
-
593
- update.color = { background: bg, border: borderColor };
594
- update.borderWidth = borderWidth;
595
- update.borderDashes = borderDashes;
596
- update.shadow = shadow;
597
-
598
- // Size
599
- if (sizeBy === 'fan-in') {
600
- update.size = 15 + Math.min(n.fanIn || 0, 30) * 2;
601
- update.shape = 'dot';
602
- } else if (sizeBy === 'fan-out') {
603
- update.size = 15 + Math.min(n.fanOut || 0, 30) * 2;
604
- update.shape = 'dot';
605
- } else if (sizeBy === 'complexity') {
606
- update.size = 15 + Math.min(n.cyclomatic || 0, 20) * 3;
607
- update.shape = 'dot';
608
- } else {
609
- update.shape = 'box';
610
- }
611
-
612
- updates.push(update);
613
- });
614
-
615
- nodes.update(updates);
616
- }
617
-
618
- /* ── Clustering ────────────────────────────────────────────────────── */
619
- function applyClusterBy(mode) {
620
- // Open all existing clusters first
621
- var ids = nodes.getIds();
622
- for (var i = 0; i < ids.length; i++) {
623
- if (network.isCluster(ids[i])) {
624
- try { network.openCluster(ids[i]); } catch(e) { /* ignore */ }
625
- }
626
- }
627
-
628
- if (mode === 'none') return;
629
-
630
- if (mode === 'community') {
631
- var communities = {};
632
- allNodes.forEach(function(n) {
633
- if (n.community !== null && visibleNodeIds.has(n.id)) {
634
- if (!communities[n.community]) communities[n.community] = [];
635
- communities[n.community].push(n.id);
636
- }
637
- });
638
- Object.keys(communities).forEach(function(cid) {
639
- if (communities[cid].length < 2) return;
640
- var cidNum = parseInt(cid, 10);
641
- network.cluster({
642
- joinCondition: function(opts) { return opts.community === cidNum; },
643
- clusterNodeProperties: {
644
- label: 'Community ' + cid,
645
- shape: 'diamond',
646
- color: communityColors[cidNum % communityColors.length]
647
- }
648
- });
649
- });
650
- } else if (mode === 'directory') {
651
- var dirs = {};
652
- allNodes.forEach(function(n) {
653
- if (visibleNodeIds.has(n.id)) {
654
- var d = n.directory || '(root)';
655
- if (!dirs[d]) dirs[d] = [];
656
- dirs[d].push(n.id);
657
- }
658
- });
659
- Object.keys(dirs).forEach(function(dir) {
660
- if (dirs[dir].length < 2) return;
661
- network.cluster({
662
- joinCondition: function(opts) { return (opts.directory || '(root)') === dir; },
663
- clusterNodeProperties: {
664
- label: dir,
665
- shape: 'diamond',
666
- color: '#B0BEC5'
667
- }
668
- });
669
- });
670
- }
671
- }
672
-
673
- /* ── Detail Panel ──────────────────────────────────────────────────── */
674
- function showDetail(nodeId) {
675
- var n = nodeById[nodeId];
676
- if (!n) { hideDetail(); return; }
677
- var adj = adjIndex[nodeId] || { callers: [], callees: [] };
678
-
679
- var h = '<h3>' + escHtml(n.label) + '</h3>';
680
- h += '<div class="detail-meta">';
681
- h += '<span class="badge kind-badge">' + escHtml(n.kind) + '</span>';
682
- if (n.role) h += '<span class="badge role-badge">' + escHtml(n.role) + '</span>';
683
- h += '</div>';
684
- h += '<div class="detail-file">' + escHtml(n.file) + ':' + n.line + '</div>';
685
-
686
- h += '<div class="detail-section"><strong>Metrics</strong><table>';
687
- h += '<tr><td>Fan-in</td><td>' + n.fanIn + '</td></tr>';
688
- h += '<tr><td>Fan-out</td><td>' + n.fanOut + '</td></tr>';
689
- if (n.cognitive !== null) h += '<tr><td>Cognitive</td><td>' + n.cognitive + '</td></tr>';
690
- if (n.cyclomatic !== null) h += '<tr><td>Cyclomatic</td><td>' + n.cyclomatic + '</td></tr>';
691
- if (n.maintainabilityIndex !== null) h += '<tr><td>MI</td><td>' + n.maintainabilityIndex.toFixed(1) + '</td></tr>';
692
- h += '</table></div>';
693
-
694
- if (n.risk && n.risk.length > 0) {
695
- h += '<div class="detail-section"><strong>Risk</strong><br>';
696
- n.risk.forEach(function(r) { h += '<span class="badge risk-badge">' + escHtml(r) + '</span>'; });
697
- h += '</div>';
698
- }
699
-
700
- if (adj.callers.length > 0) {
701
- h += '<div class="detail-section"><strong>Callers (' + adj.callers.length + ')</strong><ul>';
702
- adj.callers.forEach(function(cid) {
703
- var c = nodeById[cid];
704
- if (c) h += '<li><a onclick="focusNode(' + cid + ')">' + escHtml(c.label) + '</a></li>';
705
- });
706
- h += '</ul></div>';
707
- }
708
-
709
- if (adj.callees.length > 0) {
710
- h += '<div class="detail-section"><strong>Callees (' + adj.callees.length + ')</strong><ul>';
711
- adj.callees.forEach(function(cid) {
712
- var c = nodeById[cid];
713
- if (c) h += '<li><a onclick="focusNode(' + cid + ')">' + escHtml(c.label) + '</a></li>';
714
- });
715
- h += '</ul></div>';
716
- }
717
-
718
- document.getElementById('detailContent').innerHTML = h;
719
- document.getElementById('detail').style.display = 'block';
720
- }
721
-
722
- function hideDetail() {
723
- document.getElementById('detail').style.display = 'none';
724
- }
725
-
726
- function focusNode(nodeId) {
727
- if (drillDownActive && !visibleNodeIds.has(nodeId)) expandNode(nodeId);
728
- network.focus(nodeId, { scale: 1.2, animation: true });
729
- network.selectNodes([nodeId]);
730
- showDetail(nodeId);
731
- }
732
-
733
- /* ── Drill-down ────────────────────────────────────────────────────── */
734
- function expandNode(nodeId) {
735
- if (!drillDownActive) return;
736
- expandedNodes.add(nodeId);
737
- var adj = adjIndex[nodeId] || { callers: [], callees: [] };
738
- var newNodeData = [];
739
- adj.callers.concat(adj.callees).forEach(function(nid) {
740
- if (!visibleNodeIds.has(nid)) {
741
- visibleNodeIds.add(nid);
742
- var n = nodeById[nid];
743
- if (n) newNodeData.push(n);
744
- }
745
- });
746
- if (newNodeData.length > 0) {
747
- nodes.add(newNodeData);
748
- var newEdges = allEdges.filter(function(e) {
749
- return visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to) && !edges.get(e.id);
750
- });
751
- if (newEdges.length > 0) edges.add(newEdges);
752
- refreshNodeAppearance();
753
- }
754
- }
755
-
756
- function collapseNode(nodeId) {
757
- if (!drillDownActive) return;
758
- expandedNodes.delete(nodeId);
759
- recalculateVisibility();
760
- }
761
-
762
- function recalculateVisibility() {
763
- var newVisible = new Set(seedSet);
764
- expandedNodes.forEach(function(nid) {
765
- newVisible.add(nid);
766
- var adj = adjIndex[nid] || { callers: [], callees: [] };
767
- adj.callers.concat(adj.callees).forEach(function(id) { newVisible.add(id); });
768
- });
769
-
770
- var toRemove = [];
771
- visibleNodeIds.forEach(function(id) { if (!newVisible.has(id)) toRemove.push(id); });
772
- if (toRemove.length > 0) nodes.remove(toRemove);
773
-
774
- var toAdd = [];
775
- newVisible.forEach(function(id) {
776
- if (!visibleNodeIds.has(id) && nodeById[id]) toAdd.push(nodeById[id]);
777
- });
778
- if (toAdd.length > 0) nodes.add(toAdd);
779
-
780
- visibleNodeIds = newVisible;
781
- edges.clear();
782
- edges.add(allEdges.filter(function(e) {
783
- return visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to);
784
- }));
785
- refreshNodeAppearance();
786
- }
787
-
788
- /* ── Legend ─────────────────────────────────────────────────────────── */
789
- function updateLegend(colorBy) {
790
- var legend = document.getElementById('legend');
791
- legend.innerHTML = '';
792
- var items = {};
793
-
794
- if (colorBy === 'kind') {
795
- allNodes.forEach(function(n) { if (n.kind && visibleNodeIds.has(n.id)) items[n.kind] = nodeColorMap[n.kind] || '#ccc'; });
796
- } else if (colorBy === 'role') {
797
- allNodes.forEach(function(n) {
798
- if (visibleNodeIds.has(n.id)) {
799
- var key = n.role || n.kind;
800
- items[key] = n.role ? (roleColorMap[n.role] || '#ccc') : (nodeColorMap[n.kind] || '#ccc');
801
- }
802
- });
803
- } else if (colorBy === 'community') {
804
- allNodes.forEach(function(n) {
805
- if (n.community !== null && visibleNodeIds.has(n.id)) {
806
- items['Community ' + n.community] = communityColors[n.community % communityColors.length];
807
- }
808
- });
809
- } else if (colorBy === 'complexity') {
810
- items['MI >= 80'] = '#4CAF50';
811
- items['MI 65-80'] = '#FFC107';
812
- items['MI 40-65'] = '#FF9800';
813
- items['MI < 40'] = '#F44336';
814
- }
815
-
816
- Object.keys(items).sort().forEach(function(k) {
817
- var d = document.createElement('div');
818
- d.innerHTML = '<span class="swatch" style="background:' + items[k] + '"></span>' + escHtml(k);
819
- legend.appendChild(d);
820
- });
821
- }
822
-
823
- /* ── Network Events ────────────────────────────────────────────────── */
824
- network.on('click', function(params) {
825
- if (params.nodes.length === 1) {
826
- var nodeId = params.nodes[0];
827
- if (network.isCluster(nodeId)) {
828
- network.openCluster(nodeId);
829
- return;
830
- }
831
- if (drillDownActive && !expandedNodes.has(nodeId)) expandNode(nodeId);
832
- showDetail(nodeId);
833
- } else {
834
- hideDetail();
835
- }
836
- });
837
-
838
- network.on('doubleClick', function(params) {
839
- if (params.nodes.length === 1) {
840
- var nodeId = params.nodes[0];
841
- if (network.isCluster(nodeId)) return;
842
- if (drillDownActive && expandedNodes.has(nodeId)) collapseNode(nodeId);
843
- }
844
- });
845
-
846
- /* ── Control Events ────────────────────────────────────────────────── */
847
- document.getElementById('layoutSelect').addEventListener('change', function(e) {
848
- var val = e.target.value;
849
- if (val === 'hierarchical') {
850
- network.setOptions({ layout: { hierarchical: { enabled: true, direction: ${JSON.stringify(cfg.layout.direction || 'LR')} } }, physics: { enabled: document.getElementById('physicsToggle').checked } });
851
- } else if (val === 'radial') {
852
- network.setOptions({ layout: { hierarchical: false, improvedLayout: true }, physics: { enabled: true, solver: 'repulsion', repulsion: { nodeDistance: 200 } } });
853
- } else {
854
- network.setOptions({ layout: { hierarchical: false }, physics: { enabled: true } });
855
- }
856
- });
857
-
858
- document.getElementById('physicsToggle').addEventListener('change', function(e) {
859
- network.setOptions({ physics: { enabled: e.target.checked } });
860
- });
861
-
862
- document.getElementById('searchInput').addEventListener('input', function(e) {
863
- var q = e.target.value.toLowerCase();
864
- if (!q) {
865
- nodes.update(getVisibleNodes().map(function(n) { return { id: n.id, hidden: false }; }));
866
- return;
867
- }
868
- getVisibleNodes().forEach(function(n) {
869
- var match = n.label.toLowerCase().includes(q) || (n.file && n.file.toLowerCase().includes(q));
870
- nodes.update({ id: n.id, hidden: !match });
871
- });
872
- });
873
-
874
- document.getElementById('colorBySelect').addEventListener('change', function() {
875
- refreshNodeAppearance();
876
- updateLegend(document.getElementById('colorBySelect').value);
877
- });
878
-
879
- document.getElementById('sizeBySelect').addEventListener('change', function() {
880
- refreshNodeAppearance();
881
- });
882
-
883
- document.getElementById('clusterBySelect').addEventListener('change', function(e) {
884
- applyClusterBy(e.target.value);
885
- });
886
-
887
- document.getElementById('riskToggle').addEventListener('change', function() {
888
- refreshNodeAppearance();
889
- });
890
-
891
- document.getElementById('detailClose').addEventListener('click', hideDetail);
892
-
893
- /* ── Init ──────────────────────────────────────────────────────────── */
894
- refreshNodeAppearance();
895
- updateLegend(${JSON.stringify(effectiveColorBy)});
896
- ${(cfg.clusterBy || 'none') !== 'none' ? `applyClusterBy(${JSON.stringify(cfg.clusterBy)});` : ''}
897
- </script>
898
- </body>
899
- </html>`;
900
- }
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;
326
+ return renderPlotHTML(data, cfg);
948
327
  }