@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
@@ -0,0 +1,634 @@
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
+
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { COMMUNITY_COLORS, DEFAULT_NODE_COLORS, DEFAULT_ROLE_COLORS } from './colors.js';
18
+
19
+ // Re-export color constants so existing consumers are unaffected
20
+ export { COMMUNITY_COLORS, DEFAULT_NODE_COLORS, DEFAULT_ROLE_COLORS };
21
+
22
+ export const DEFAULT_CONFIG = {
23
+ layout: { algorithm: 'hierarchical', direction: 'LR' },
24
+ physics: { enabled: true, nodeDistance: 150 },
25
+ nodeColors: DEFAULT_NODE_COLORS,
26
+ roleColors: DEFAULT_ROLE_COLORS,
27
+ colorBy: 'kind',
28
+ edgeStyle: { color: '#666', smooth: true },
29
+ filter: { kinds: null, roles: null, files: null },
30
+ title: 'Codegraph',
31
+ seedStrategy: 'all',
32
+ seedCount: 30,
33
+ clusterBy: 'none',
34
+ sizeBy: 'uniform',
35
+ overlays: { complexity: false, risk: false },
36
+ riskThresholds: { highBlastRadius: 10, lowMI: 40 },
37
+ };
38
+
39
+ // ─── Config Loading ──────────────────────────────────────────────────
40
+
41
+ /**
42
+ * Load .plotDotCfg or .plotDotCfg.json from given directory.
43
+ * Returns merged config with defaults.
44
+ */
45
+ export function loadPlotConfig(dir) {
46
+ for (const name of ['.plotDotCfg', '.plotDotCfg.json']) {
47
+ const p = path.join(dir, name);
48
+ if (fs.existsSync(p)) {
49
+ try {
50
+ const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
51
+ return {
52
+ ...DEFAULT_CONFIG,
53
+ ...raw,
54
+ layout: { ...DEFAULT_CONFIG.layout, ...(raw.layout || {}) },
55
+ physics: { ...DEFAULT_CONFIG.physics, ...(raw.physics || {}) },
56
+ nodeColors: {
57
+ ...DEFAULT_CONFIG.nodeColors,
58
+ ...(raw.nodeColors || {}),
59
+ },
60
+ roleColors: {
61
+ ...DEFAULT_CONFIG.roleColors,
62
+ ...(raw.roleColors || {}),
63
+ },
64
+ edgeStyle: {
65
+ ...DEFAULT_CONFIG.edgeStyle,
66
+ ...(raw.edgeStyle || {}),
67
+ },
68
+ filter: { ...DEFAULT_CONFIG.filter, ...(raw.filter || {}) },
69
+ overlays: {
70
+ ...DEFAULT_CONFIG.overlays,
71
+ ...(raw.overlays || {}),
72
+ },
73
+ riskThresholds: {
74
+ ...DEFAULT_CONFIG.riskThresholds,
75
+ ...(raw.riskThresholds || {}),
76
+ },
77
+ };
78
+ } catch {
79
+ // Invalid JSON — use defaults
80
+ }
81
+ }
82
+ }
83
+ return { ...DEFAULT_CONFIG };
84
+ }
85
+
86
+ // ─── Internal Helpers ────────────────────────────────────────────────
87
+
88
+ export function escapeHtml(s) {
89
+ return String(s)
90
+ .replace(/&/g, '&')
91
+ .replace(/</g, '&lt;')
92
+ .replace(/>/g, '&gt;')
93
+ .replace(/"/g, '&quot;');
94
+ }
95
+
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
+ };
119
+
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
+ },
128
+ };
129
+ }
130
+
131
+ return opts;
132
+ }
133
+
134
+ // ─── HTML Renderer ───────────────────────────────────────────────────
135
+
136
+ /**
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
144
+ */
145
+ export function renderPlotHTML(data, cfg) {
146
+ const layoutOpts = buildLayoutOptions(cfg);
147
+ const title = cfg.title || 'Codegraph';
148
+
149
+ // Resolve effective colorBy (overlays.complexity overrides)
150
+ const effectiveColorBy =
151
+ cfg.overlays?.complexity && cfg.colorBy === 'kind' ? 'complexity' : cfg.colorBy || 'kind';
152
+ const effectiveRisk = cfg.overlays?.risk || false;
153
+
154
+ return `<!DOCTYPE html>
155
+ <html lang="en">
156
+ <head>
157
+ <meta charset="UTF-8">
158
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
159
+ <title>${escapeHtml(title)}</title>
160
+ <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
161
+ <style>
162
+ * { margin: 0; padding: 0; box-sizing: border-box; }
163
+ body { font-family: monospace; background: #fafafa; }
164
+ #controls { padding: 8px 12px; background: #fff; border-bottom: 1px solid #ddd; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
165
+ #controls label { font-size: 13px; }
166
+ #controls select, #controls input[type="text"] { font-size: 13px; padding: 2px 6px; }
167
+ #main { display: flex; height: calc(100vh - 44px); }
168
+ #graph { flex: 1; }
169
+ #detail { width: 320px; border-left: 1px solid #ddd; background: #fff; overflow-y: auto; display: none; padding: 12px; font-size: 13px; }
170
+ #detail h3 { margin-bottom: 6px; word-break: break-all; }
171
+ #detailClose { float: right; cursor: pointer; font-size: 18px; color: #999; line-height: 1; }
172
+ #detailClose:hover { color: #333; }
173
+ .detail-meta { margin-bottom: 4px; }
174
+ .detail-file { color: #666; margin-bottom: 10px; font-size: 12px; }
175
+ .detail-section { margin-bottom: 10px; }
176
+ .detail-section table { width: 100%; border-collapse: collapse; }
177
+ .detail-section td { padding: 2px 8px 2px 0; }
178
+ .detail-section ul { list-style: none; padding: 0; }
179
+ .detail-section li { padding: 2px 0; }
180
+ .detail-section a { color: #1976D2; text-decoration: none; cursor: pointer; }
181
+ .detail-section a:hover { text-decoration: underline; }
182
+ .badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; margin-right: 4px; }
183
+ .kind-badge { background: #E3F2FD; color: #1565C0; }
184
+ .role-badge { background: #E8F5E9; color: #2E7D32; }
185
+ .risk-badge { background: #FFEBEE; color: #C62828; }
186
+ #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; }
187
+ #legend div { display: flex; align-items: center; gap: 6px; margin: 2px 0; }
188
+ #legend span.swatch { width: 14px; height: 14px; border-radius: 3px; display: inline-block; flex-shrink: 0; }
189
+ </style>
190
+ </head>
191
+ <body>
192
+ <div id="controls">
193
+ <label>Layout:
194
+ <select id="layoutSelect">
195
+ <option value="hierarchical"${cfg.layout.algorithm === 'hierarchical' ? ' selected' : ''}>Hierarchical</option>
196
+ <option value="force"${cfg.layout.algorithm === 'force' ? ' selected' : ''}>Force</option>
197
+ <option value="radial"${cfg.layout.algorithm === 'radial' ? ' selected' : ''}>Radial</option>
198
+ </select>
199
+ </label>
200
+ <label>Physics: <input type="checkbox" id="physicsToggle"${cfg.physics.enabled ? ' checked' : ''}></label>
201
+ <label>Search: <input type="text" id="searchInput" placeholder="Filter nodes..."></label>
202
+ <label>Color by:
203
+ <select id="colorBySelect">
204
+ <option value="kind"${effectiveColorBy === 'kind' ? ' selected' : ''}>Kind</option>
205
+ <option value="role"${effectiveColorBy === 'role' ? ' selected' : ''}>Role</option>
206
+ <option value="community"${effectiveColorBy === 'community' ? ' selected' : ''}>Community</option>
207
+ <option value="complexity"${effectiveColorBy === 'complexity' ? ' selected' : ''}>Complexity</option>
208
+ </select>
209
+ </label>
210
+ <label>Size by:
211
+ <select id="sizeBySelect">
212
+ <option value="uniform"${(cfg.sizeBy || 'uniform') === 'uniform' ? ' selected' : ''}>Uniform</option>
213
+ <option value="fan-in"${cfg.sizeBy === 'fan-in' ? ' selected' : ''}>Fan-in</option>
214
+ <option value="fan-out"${cfg.sizeBy === 'fan-out' ? ' selected' : ''}>Fan-out</option>
215
+ <option value="complexity"${cfg.sizeBy === 'complexity' ? ' selected' : ''}>Complexity</option>
216
+ </select>
217
+ </label>
218
+ <label>Cluster by:
219
+ <select id="clusterBySelect">
220
+ <option value="none"${(cfg.clusterBy || 'none') === 'none' ? ' selected' : ''}>None</option>
221
+ <option value="community"${cfg.clusterBy === 'community' ? ' selected' : ''}>Community</option>
222
+ <option value="directory"${cfg.clusterBy === 'directory' ? ' selected' : ''}>Directory</option>
223
+ </select>
224
+ </label>
225
+ <label>Risk: <input type="checkbox" id="riskToggle"${effectiveRisk ? ' checked' : ''}></label>
226
+ </div>
227
+ <div id="main">
228
+ <div id="graph"></div>
229
+ <div id="detail">
230
+ <span id="detailClose">&times;</span>
231
+ <div id="detailContent"></div>
232
+ </div>
233
+ </div>
234
+ <div id="legend"></div>
235
+ <script>
236
+ /* ── Data ──────────────────────────────────────────────────────────── */
237
+ var allNodes = ${JSON.stringify(data.nodes)};
238
+ var allEdges = ${JSON.stringify(data.edges)};
239
+ var seedNodeIds = ${JSON.stringify(data.seedNodeIds)};
240
+ var nodeColorMap = ${JSON.stringify(cfg.nodeColors || DEFAULT_NODE_COLORS)};
241
+ var roleColorMap = ${JSON.stringify(cfg.roleColors || DEFAULT_ROLE_COLORS)};
242
+ var communityColors = ${JSON.stringify(COMMUNITY_COLORS)};
243
+
244
+ /* ── Lookups ───────────────────────────────────────────────────────── */
245
+ var nodeById = {};
246
+ allNodes.forEach(function(n) { nodeById[n.id] = n; });
247
+ var adjIndex = {};
248
+ allNodes.forEach(function(n) { adjIndex[n.id] = { callers: [], callees: [] }; });
249
+ allEdges.forEach(function(e) {
250
+ if (adjIndex[e.from]) adjIndex[e.from].callees.push(e.to);
251
+ if (adjIndex[e.to]) adjIndex[e.to].callers.push(e.from);
252
+ });
253
+
254
+ /* ── State ─────────────────────────────────────────────────────────── */
255
+ var seedSet = new Set(seedNodeIds);
256
+ var visibleNodeIds = new Set(seedNodeIds);
257
+ var expandedNodes = new Set();
258
+ var drillDownActive = ${JSON.stringify((cfg.seedStrategy || 'all') !== 'all')};
259
+
260
+ /* ── Helpers ───────────────────────────────────────────────────────── */
261
+ function escHtml(s) {
262
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
263
+ }
264
+
265
+ /* ── vis-network init ──────────────────────────────────────────────── */
266
+ function getVisibleNodes() {
267
+ return allNodes.filter(function(n) { return visibleNodeIds.has(n.id); });
268
+ }
269
+ function getVisibleEdges() {
270
+ return allEdges.filter(function(e) { return visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to); });
271
+ }
272
+
273
+ var nodes = new vis.DataSet(getVisibleNodes());
274
+ var edges = new vis.DataSet(getVisibleEdges());
275
+ var container = document.getElementById('graph');
276
+ var options = ${JSON.stringify(layoutOpts, null, 2)};
277
+ var network = new vis.Network(container, { nodes: nodes, edges: edges }, options);
278
+
279
+ /* ── Appearance ────────────────────────────────────────────────────── */
280
+ function refreshNodeAppearance() {
281
+ var colorBy = document.getElementById('colorBySelect').value;
282
+ var sizeBy = document.getElementById('sizeBySelect').value;
283
+ var riskEnabled = document.getElementById('riskToggle').checked;
284
+ var updates = [];
285
+
286
+ allNodes.forEach(function(n) {
287
+ if (!visibleNodeIds.has(n.id)) return;
288
+ var update = { id: n.id };
289
+
290
+ // Background color
291
+ var bg;
292
+ if (colorBy === 'role') {
293
+ bg = n.role ? (roleColorMap[n.role] || nodeColorMap[n.kind] || '#ccc') : (nodeColorMap[n.kind] || '#ccc');
294
+ } else if (colorBy === 'community') {
295
+ bg = n.community !== null ? communityColors[n.community % communityColors.length] : '#ccc';
296
+ } else {
297
+ bg = nodeColorMap[n.kind] || '#ccc';
298
+ }
299
+
300
+ var borderColor = '#888';
301
+ var borderWidth = 1;
302
+ var borderDashes = false;
303
+ var shadow = false;
304
+
305
+ // Complexity border (when colorBy is 'complexity')
306
+ if (colorBy === 'complexity' && n.maintainabilityIndex !== null) {
307
+ var mi = n.maintainabilityIndex;
308
+ if (mi >= 80) { borderColor = '#4CAF50'; borderWidth = 2; }
309
+ else if (mi >= 65) { borderColor = '#FFC107'; borderWidth = 3; }
310
+ else if (mi >= 40) { borderColor = '#FF9800'; borderWidth = 3; }
311
+ else { borderColor = '#F44336'; borderWidth = 4; }
312
+ }
313
+
314
+ // Risk overlay (overrides border when active)
315
+ if (riskEnabled && n.risk && n.risk.length > 0) {
316
+ if (n.risk.indexOf('dead-code') >= 0) {
317
+ borderColor = '#F44336'; borderDashes = [5, 5]; borderWidth = 3;
318
+ }
319
+ if (n.risk.indexOf('high-blast-radius') >= 0) {
320
+ borderColor = '#FF9800'; shadow = true; borderWidth = 3;
321
+ }
322
+ if (n.risk.indexOf('low-mi') >= 0) {
323
+ borderColor = '#FF9800'; borderWidth = 3;
324
+ }
325
+ }
326
+
327
+ update.color = { background: bg, border: borderColor };
328
+ update.borderWidth = borderWidth;
329
+ update.borderDashes = borderDashes;
330
+ update.shadow = shadow;
331
+
332
+ // Size
333
+ if (sizeBy === 'fan-in') {
334
+ update.size = 15 + Math.min(n.fanIn || 0, 30) * 2;
335
+ update.shape = 'dot';
336
+ } else if (sizeBy === 'fan-out') {
337
+ update.size = 15 + Math.min(n.fanOut || 0, 30) * 2;
338
+ update.shape = 'dot';
339
+ } else if (sizeBy === 'complexity') {
340
+ update.size = 15 + Math.min(n.cyclomatic || 0, 20) * 3;
341
+ update.shape = 'dot';
342
+ } else {
343
+ update.shape = 'box';
344
+ }
345
+
346
+ updates.push(update);
347
+ });
348
+
349
+ nodes.update(updates);
350
+ }
351
+
352
+ /* ── Clustering ────────────────────────────────────────────────────── */
353
+ function applyClusterBy(mode) {
354
+ // Open all existing clusters first
355
+ var ids = nodes.getIds();
356
+ for (var i = 0; i < ids.length; i++) {
357
+ if (network.isCluster(ids[i])) {
358
+ try { network.openCluster(ids[i]); } catch(e) { /* ignore */ }
359
+ }
360
+ }
361
+
362
+ if (mode === 'none') return;
363
+
364
+ if (mode === 'community') {
365
+ var communities = {};
366
+ allNodes.forEach(function(n) {
367
+ if (n.community !== null && visibleNodeIds.has(n.id)) {
368
+ if (!communities[n.community]) communities[n.community] = [];
369
+ communities[n.community].push(n.id);
370
+ }
371
+ });
372
+ Object.keys(communities).forEach(function(cid) {
373
+ if (communities[cid].length < 2) return;
374
+ var cidNum = parseInt(cid, 10);
375
+ network.cluster({
376
+ joinCondition: function(opts) { return opts.community === cidNum; },
377
+ clusterNodeProperties: {
378
+ label: 'Community ' + cid,
379
+ shape: 'diamond',
380
+ color: communityColors[cidNum % communityColors.length]
381
+ }
382
+ });
383
+ });
384
+ } else if (mode === 'directory') {
385
+ var dirs = {};
386
+ allNodes.forEach(function(n) {
387
+ if (visibleNodeIds.has(n.id)) {
388
+ var d = n.directory || '(root)';
389
+ if (!dirs[d]) dirs[d] = [];
390
+ dirs[d].push(n.id);
391
+ }
392
+ });
393
+ Object.keys(dirs).forEach(function(dir) {
394
+ if (dirs[dir].length < 2) return;
395
+ network.cluster({
396
+ joinCondition: function(opts) { return (opts.directory || '(root)') === dir; },
397
+ clusterNodeProperties: {
398
+ label: dir,
399
+ shape: 'diamond',
400
+ color: '#B0BEC5'
401
+ }
402
+ });
403
+ });
404
+ }
405
+ }
406
+
407
+ /* ── Detail Panel ──────────────────────────────────────────────────── */
408
+ function showDetail(nodeId) {
409
+ var n = nodeById[nodeId];
410
+ if (!n) { hideDetail(); return; }
411
+ var adj = adjIndex[nodeId] || { callers: [], callees: [] };
412
+
413
+ var h = '<h3>' + escHtml(n.label) + '</h3>';
414
+ h += '<div class="detail-meta">';
415
+ h += '<span class="badge kind-badge">' + escHtml(n.kind) + '</span>';
416
+ if (n.role) h += '<span class="badge role-badge">' + escHtml(n.role) + '</span>';
417
+ h += '</div>';
418
+ h += '<div class="detail-file">' + escHtml(n.file) + ':' + n.line + '</div>';
419
+
420
+ h += '<div class="detail-section"><strong>Metrics</strong><table>';
421
+ h += '<tr><td>Fan-in</td><td>' + n.fanIn + '</td></tr>';
422
+ h += '<tr><td>Fan-out</td><td>' + n.fanOut + '</td></tr>';
423
+ if (n.cognitive !== null) h += '<tr><td>Cognitive</td><td>' + n.cognitive + '</td></tr>';
424
+ if (n.cyclomatic !== null) h += '<tr><td>Cyclomatic</td><td>' + n.cyclomatic + '</td></tr>';
425
+ if (n.maintainabilityIndex !== null) h += '<tr><td>MI</td><td>' + n.maintainabilityIndex.toFixed(1) + '</td></tr>';
426
+ h += '</table></div>';
427
+
428
+ if (n.risk && n.risk.length > 0) {
429
+ h += '<div class="detail-section"><strong>Risk</strong><br>';
430
+ n.risk.forEach(function(r) { h += '<span class="badge risk-badge">' + escHtml(r) + '</span>'; });
431
+ h += '</div>';
432
+ }
433
+
434
+ if (adj.callers.length > 0) {
435
+ h += '<div class="detail-section"><strong>Callers (' + adj.callers.length + ')</strong><ul>';
436
+ adj.callers.forEach(function(cid) {
437
+ var c = nodeById[cid];
438
+ if (c) h += '<li><a onclick="focusNode(' + cid + ')">' + escHtml(c.label) + '</a></li>';
439
+ });
440
+ h += '</ul></div>';
441
+ }
442
+
443
+ if (adj.callees.length > 0) {
444
+ h += '<div class="detail-section"><strong>Callees (' + adj.callees.length + ')</strong><ul>';
445
+ adj.callees.forEach(function(cid) {
446
+ var c = nodeById[cid];
447
+ if (c) h += '<li><a onclick="focusNode(' + cid + ')">' + escHtml(c.label) + '</a></li>';
448
+ });
449
+ h += '</ul></div>';
450
+ }
451
+
452
+ document.getElementById('detailContent').innerHTML = h;
453
+ document.getElementById('detail').style.display = 'block';
454
+ }
455
+
456
+ function hideDetail() {
457
+ document.getElementById('detail').style.display = 'none';
458
+ }
459
+
460
+ function focusNode(nodeId) {
461
+ if (drillDownActive && !visibleNodeIds.has(nodeId)) expandNode(nodeId);
462
+ network.focus(nodeId, { scale: 1.2, animation: true });
463
+ network.selectNodes([nodeId]);
464
+ showDetail(nodeId);
465
+ }
466
+
467
+ /* ── Drill-down ────────────────────────────────────────────────────── */
468
+ function expandNode(nodeId) {
469
+ if (!drillDownActive) return;
470
+ expandedNodes.add(nodeId);
471
+ var adj = adjIndex[nodeId] || { callers: [], callees: [] };
472
+ var newNodeData = [];
473
+ adj.callers.concat(adj.callees).forEach(function(nid) {
474
+ if (!visibleNodeIds.has(nid)) {
475
+ visibleNodeIds.add(nid);
476
+ var n = nodeById[nid];
477
+ if (n) newNodeData.push(n);
478
+ }
479
+ });
480
+ if (newNodeData.length > 0) {
481
+ nodes.add(newNodeData);
482
+ var newEdges = allEdges.filter(function(e) {
483
+ return visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to) && !edges.get(e.id);
484
+ });
485
+ if (newEdges.length > 0) edges.add(newEdges);
486
+ refreshNodeAppearance();
487
+ }
488
+ }
489
+
490
+ function collapseNode(nodeId) {
491
+ if (!drillDownActive) return;
492
+ expandedNodes.delete(nodeId);
493
+ recalculateVisibility();
494
+ }
495
+
496
+ function recalculateVisibility() {
497
+ var newVisible = new Set(seedSet);
498
+ expandedNodes.forEach(function(nid) {
499
+ newVisible.add(nid);
500
+ var adj = adjIndex[nid] || { callers: [], callees: [] };
501
+ adj.callers.concat(adj.callees).forEach(function(id) { newVisible.add(id); });
502
+ });
503
+
504
+ var toRemove = [];
505
+ visibleNodeIds.forEach(function(id) { if (!newVisible.has(id)) toRemove.push(id); });
506
+ if (toRemove.length > 0) nodes.remove(toRemove);
507
+
508
+ var toAdd = [];
509
+ newVisible.forEach(function(id) {
510
+ if (!visibleNodeIds.has(id) && nodeById[id]) toAdd.push(nodeById[id]);
511
+ });
512
+ if (toAdd.length > 0) nodes.add(toAdd);
513
+
514
+ visibleNodeIds = newVisible;
515
+ edges.clear();
516
+ edges.add(allEdges.filter(function(e) {
517
+ return visibleNodeIds.has(e.from) && visibleNodeIds.has(e.to);
518
+ }));
519
+ refreshNodeAppearance();
520
+ }
521
+
522
+ /* ── Legend ─────────────────────────────────────────────────────────── */
523
+ function updateLegend(colorBy) {
524
+ var legend = document.getElementById('legend');
525
+ legend.innerHTML = '';
526
+ var items = {};
527
+
528
+ if (colorBy === 'kind') {
529
+ allNodes.forEach(function(n) { if (n.kind && visibleNodeIds.has(n.id)) items[n.kind] = nodeColorMap[n.kind] || '#ccc'; });
530
+ } else if (colorBy === 'role') {
531
+ allNodes.forEach(function(n) {
532
+ if (visibleNodeIds.has(n.id)) {
533
+ var key = n.role || n.kind;
534
+ items[key] = n.role ? (roleColorMap[n.role] || '#ccc') : (nodeColorMap[n.kind] || '#ccc');
535
+ }
536
+ });
537
+ } else if (colorBy === 'community') {
538
+ allNodes.forEach(function(n) {
539
+ if (n.community !== null && visibleNodeIds.has(n.id)) {
540
+ items['Community ' + n.community] = communityColors[n.community % communityColors.length];
541
+ }
542
+ });
543
+ } else if (colorBy === 'complexity') {
544
+ items['MI >= 80'] = '#4CAF50';
545
+ items['MI 65-80'] = '#FFC107';
546
+ items['MI 40-65'] = '#FF9800';
547
+ items['MI < 40'] = '#F44336';
548
+ }
549
+
550
+ Object.keys(items).sort().forEach(function(k) {
551
+ var d = document.createElement('div');
552
+ d.innerHTML = '<span class="swatch" style="background:' + items[k] + '"></span>' + escHtml(k);
553
+ legend.appendChild(d);
554
+ });
555
+ }
556
+
557
+ /* ── Network Events ────────────────────────────────────────────────── */
558
+ network.on('click', function(params) {
559
+ if (params.nodes.length === 1) {
560
+ var nodeId = params.nodes[0];
561
+ if (network.isCluster(nodeId)) {
562
+ network.openCluster(nodeId);
563
+ return;
564
+ }
565
+ if (drillDownActive && !expandedNodes.has(nodeId)) expandNode(nodeId);
566
+ showDetail(nodeId);
567
+ } else {
568
+ hideDetail();
569
+ }
570
+ });
571
+
572
+ network.on('doubleClick', function(params) {
573
+ if (params.nodes.length === 1) {
574
+ var nodeId = params.nodes[0];
575
+ if (network.isCluster(nodeId)) return;
576
+ if (drillDownActive && expandedNodes.has(nodeId)) collapseNode(nodeId);
577
+ }
578
+ });
579
+
580
+ /* ── Control Events ────────────────────────────────────────────────── */
581
+ document.getElementById('layoutSelect').addEventListener('change', function(e) {
582
+ var val = e.target.value;
583
+ if (val === 'hierarchical') {
584
+ network.setOptions({ layout: { hierarchical: { enabled: true, direction: ${JSON.stringify(cfg.layout.direction || 'LR')} } }, physics: { enabled: document.getElementById('physicsToggle').checked } });
585
+ } else if (val === 'radial') {
586
+ network.setOptions({ layout: { hierarchical: false, improvedLayout: true }, physics: { enabled: true, solver: 'repulsion', repulsion: { nodeDistance: 200 } } });
587
+ } else {
588
+ network.setOptions({ layout: { hierarchical: false }, physics: { enabled: true } });
589
+ }
590
+ });
591
+
592
+ document.getElementById('physicsToggle').addEventListener('change', function(e) {
593
+ network.setOptions({ physics: { enabled: e.target.checked } });
594
+ });
595
+
596
+ document.getElementById('searchInput').addEventListener('input', function(e) {
597
+ var q = e.target.value.toLowerCase();
598
+ if (!q) {
599
+ nodes.update(getVisibleNodes().map(function(n) { return { id: n.id, hidden: false }; }));
600
+ return;
601
+ }
602
+ getVisibleNodes().forEach(function(n) {
603
+ var match = n.label.toLowerCase().includes(q) || (n.file && n.file.toLowerCase().includes(q));
604
+ nodes.update({ id: n.id, hidden: !match });
605
+ });
606
+ });
607
+
608
+ document.getElementById('colorBySelect').addEventListener('change', function() {
609
+ refreshNodeAppearance();
610
+ updateLegend(document.getElementById('colorBySelect').value);
611
+ });
612
+
613
+ document.getElementById('sizeBySelect').addEventListener('change', function() {
614
+ refreshNodeAppearance();
615
+ });
616
+
617
+ document.getElementById('clusterBySelect').addEventListener('change', function(e) {
618
+ applyClusterBy(e.target.value);
619
+ });
620
+
621
+ document.getElementById('riskToggle').addEventListener('change', function() {
622
+ refreshNodeAppearance();
623
+ });
624
+
625
+ document.getElementById('detailClose').addEventListener('click', hideDetail);
626
+
627
+ /* ── Init ──────────────────────────────────────────────────────────── */
628
+ refreshNodeAppearance();
629
+ updateLegend(${JSON.stringify(effectiveColorBy)});
630
+ ${(cfg.clusterBy || 'none') !== 'none' ? `applyClusterBy(${JSON.stringify(cfg.clusterBy)});` : ''}
631
+ </script>
632
+ </body>
633
+ </html>`;
634
+ }