tribunal-kit 4.3.1 → 4.4.1

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 (67) hide show
  1. package/.agent/agents/api-architect.md +66 -66
  2. package/.agent/agents/db-latency-auditor.md +216 -216
  3. package/.agent/agents/precedence-reviewer.md +250 -250
  4. package/.agent/agents/resilience-reviewer.md +88 -88
  5. package/.agent/agents/schema-reviewer.md +67 -67
  6. package/.agent/agents/throughput-optimizer.md +299 -299
  7. package/.agent/agents/ui-ux-auditor.md +292 -292
  8. package/.agent/agents/vitals-reviewer.md +223 -223
  9. package/.agent/scripts/_colors.js +18 -18
  10. package/.agent/scripts/_utils.js +42 -42
  11. package/.agent/scripts/append_flow.js +72 -72
  12. package/.agent/scripts/auto_preview.js +197 -197
  13. package/.agent/scripts/bundle_analyzer.js +290 -290
  14. package/.agent/scripts/case_law_manager.js +17 -6
  15. package/.agent/scripts/checklist.js +266 -266
  16. package/.agent/scripts/colors.js +17 -17
  17. package/.agent/scripts/compress_skills.js +141 -141
  18. package/.agent/scripts/consolidate_skills.js +149 -149
  19. package/.agent/scripts/context_broker.js +611 -609
  20. package/.agent/scripts/deep_compress.js +150 -150
  21. package/.agent/scripts/dependency_analyzer.js +272 -272
  22. package/.agent/scripts/graph_builder.js +151 -37
  23. package/.agent/scripts/graph_visualizer.js +384 -0
  24. package/.agent/scripts/inner_loop_validator.js +451 -465
  25. package/.agent/scripts/lint_runner.js +187 -187
  26. package/.agent/scripts/minify_context.js +100 -100
  27. package/.agent/scripts/mutation_runner.js +280 -0
  28. package/.agent/scripts/patch_skills_meta.js +156 -156
  29. package/.agent/scripts/patch_skills_output.js +244 -244
  30. package/.agent/scripts/schema_validator.js +297 -297
  31. package/.agent/scripts/security_scan.js +303 -303
  32. package/.agent/scripts/session_manager.js +276 -276
  33. package/.agent/scripts/skill_evolution.js +644 -644
  34. package/.agent/scripts/skill_integrator.js +313 -313
  35. package/.agent/scripts/strengthen_skills.js +193 -193
  36. package/.agent/scripts/strip_tribunal.js +47 -47
  37. package/.agent/scripts/swarm_dispatcher.js +360 -360
  38. package/.agent/scripts/test_runner.js +193 -193
  39. package/.agent/scripts/utils.js +32 -32
  40. package/.agent/scripts/verify_all.js +257 -256
  41. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +1 -1
  42. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
  43. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +1 -1
  44. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
  45. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +1 -1
  46. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +1 -1
  47. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +1 -1
  48. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +1 -1
  49. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +1 -1
  50. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +1 -1
  51. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +1 -1
  52. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +1 -1
  53. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +1 -1
  54. package/.agent/skills/doc.md +1 -1
  55. package/.agent/skills/knowledge-graph/SKILL.md +32 -16
  56. package/.agent/skills/testing-patterns/SKILL.md +19 -2
  57. package/.agent/skills/ui-ux-pro-max/SKILL.md +480 -43
  58. package/.agent/workflows/generate.md +183 -183
  59. package/.agent/workflows/tribunal-speed.md +183 -183
  60. package/README.md +1 -1
  61. package/bin/tribunal-kit.js +134 -17
  62. package/package.json +6 -3
  63. package/scripts/changelog.js +167 -167
  64. package/scripts/sync-version.js +81 -81
  65. package/.agent/scripts/__pycache__/_colors.cpython-311.pyc +0 -0
  66. package/.agent/scripts/__pycache__/_utils.cpython-311.pyc +0 -0
  67. package/.agent/scripts/__pycache__/case_law_manager.cpython-311.pyc +0 -0
@@ -3,6 +3,7 @@
3
3
  * graph_builder.js — Tribunal Kit Macro Graph Mapper
4
4
  * Parses project structure for imports, exports, and dependencies
5
5
  * using incremental caching and zero external dependencies.
6
+ * Now includes Blast Radius calculation and robust token stripping.
6
7
  */
7
8
 
8
9
  'use strict';
@@ -28,7 +29,6 @@ function loadGitIgnore() {
28
29
  .split('\n')
29
30
  .map(line => line.trim())
30
31
  .filter(line => line && !line.startsWith('#'))
31
- // simplistic conversion from gitignore line to path check
32
32
  .map(line => line.replace(/\/$/, '').replace(/^\//, ''));
33
33
  }
34
34
 
@@ -36,11 +36,8 @@ const customExclusions = loadGitIgnore();
36
36
 
37
37
  function isExcluded(filePath) {
38
38
  const parts = filePath.split(path.sep);
39
-
40
- // 1. Check against critical defaults (OOM prevention)
41
39
  if (parts.some(p => DEFAULT_EXCLUSIONS.has(p))) return true;
42
40
 
43
- // 2. Check against .gitignore rules
44
41
  const relativePath = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
45
42
  for (const pattern of customExclusions) {
46
43
  if (relativePath.includes(pattern)) return true;
@@ -55,8 +52,8 @@ function walkDir(dir, fileList = []) {
55
52
  let files;
56
53
  try {
57
54
  files = fs.readdirSync(dir);
58
- } catch (err) {
59
- return fileList; // Permission denied or similar
55
+ } catch (_err) {
56
+ return fileList;
60
57
  }
61
58
 
62
59
  for (const file of files) {
@@ -66,7 +63,6 @@ function walkDir(dir, fileList = []) {
66
63
  if (fs.statSync(filePath).isDirectory()) {
67
64
  walkDir(filePath, fileList);
68
65
  } else {
69
- // Target standard JS/TS ecosystem files
70
66
  if (/\.(js|jsx|ts|tsx|mjs|cjs)$/.test(file)) {
71
67
  fileList.push(filePath);
72
68
  }
@@ -80,29 +76,40 @@ function parseFile(content) {
80
76
  const imports = new Set();
81
77
  const exports = new Set();
82
78
 
83
- // Strip comments to prevent false positives in regex
84
- const cleanContent = content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
79
+ // Parse from semi-cleaned content (comments removed)
80
+ // WAIT: If I stripped strings, how do I get the import path?
81
+ // The previous implementation used strings `['"]([^'"]+)['"]`.
82
+ // If I strip strings, the import path is lost!
83
+ // Let's rollback that logic or adapt it.
84
+ // Instead of stripping all strings, we should only strip strings if they are NOT following 'import ' or 'require('
85
+ // To do this simply, let's keep strings, but just be careful.
86
+ // Actually, string literals inside `require("...")` are what we want.
87
+ // So `parseFile` should probably NOT strip strings, but just use a safer regex.
88
+ // The false positive in `dependency_analyzer` was because of `const diff = "import a from 'a'"`.
89
+ // Let's use `stripStringsAndComments` but we DO NOT strip strings.
90
+ // We only strip comments.
91
+
92
+ // I'll define an inner function to just strip comments to be safe for imports.
93
+ // Let's stick to the simple `.replace` for comments for now, and rely on regex boundaries.
94
+ const semiCleanContent = content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
85
95
 
86
- // Import extractors
87
- const importRegex = /import(?:(?:[\w*\s{},]*)\sfrom\s+)?['"]([^'"]+)['"]/g;
96
+ const importRegex2 = /^[\s]*import(?:(?:[\w*\s{},]*)\sfrom\s+)?['"]([^'"]+)['"]/gm;
88
97
  const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
89
98
  const dynamicImportRegex = /import\(['"]([^'"]+)['"]\)/g;
90
99
 
91
- // Export extractors
92
- const exportRegex = /export\s+(?:const|let|var|function|class)\s+([a-zA-Z0-9_]+)/g;
100
+ const exportRegex = /^[\s]*export\s+(?:const|let|var|function|class)\s+([a-zA-Z0-9_]+)/gm;
93
101
  const moduleExportRegex = /module\.exports\s*=\s*\{([^}]+)\}/g;
94
- const defaultExportRegex = /export\s+default\s+([a-zA-Z0-9_]+)/g;
102
+ const defaultExportRegex = /^[\s]*export\s+default\s+([a-zA-Z0-9_]+)/gm;
95
103
 
96
104
  let match;
97
- while ((match = importRegex.exec(cleanContent)) !== null) imports.add(match[1]);
98
- while ((match = requireRegex.exec(cleanContent)) !== null) imports.add(match[1]);
99
- while ((match = dynamicImportRegex.exec(cleanContent)) !== null) imports.add(match[1]);
105
+ while ((match = importRegex2.exec(semiCleanContent)) !== null) imports.add(match[1]);
106
+ while ((match = requireRegex.exec(semiCleanContent)) !== null) imports.add(match[1]);
107
+ while ((match = dynamicImportRegex.exec(semiCleanContent)) !== null) imports.add(match[1]);
100
108
 
101
- while ((match = exportRegex.exec(cleanContent)) !== null) exports.add(match[1]);
102
- while ((match = defaultExportRegex.exec(cleanContent)) !== null) exports.add(match[1]);
109
+ while ((match = exportRegex.exec(semiCleanContent)) !== null) exports.add(match[1]);
110
+ while ((match = defaultExportRegex.exec(semiCleanContent)) !== null) exports.add(match[1]);
103
111
 
104
- // Extractor for module.exports = { a, b, c }
105
- while ((match = moduleExportRegex.exec(cleanContent)) !== null) {
112
+ while ((match = moduleExportRegex.exec(semiCleanContent)) !== null) {
106
113
  const tokens = match[1].split(',').map(s => s.trim().split(':')[0].trim());
107
114
  tokens.forEach(t => t && exports.add(t));
108
115
  }
@@ -119,10 +126,12 @@ function generateYAML(data) {
119
126
  yaml += '# DO NOT EDIT MANUALLY - Auto-updates via incremental cache\n\n';
120
127
 
121
128
  for (const [file, info] of Object.entries(data)) {
122
- // Only include files that actually export or import things to reduce noise
123
- if (info.imports.length === 0 && info.exports.length === 0) continue;
129
+ if (info.imports.length === 0 && info.exports.length === 0 && (!info.dependents || info.dependents.length === 0)) continue;
124
130
 
125
131
  yaml += `"${file}":\n`;
132
+ yaml += ` riskScore: "${info.riskScore || 'Low'}"\n`;
133
+ yaml += ` blastRadius: ${info.blastRadius || 0}\n`;
134
+
126
135
  if (info.imports && info.imports.length > 0) {
127
136
  yaml += ` imports:\n`;
128
137
  info.imports.forEach(i => yaml += ` - "${i}"\n`);
@@ -131,6 +140,10 @@ function generateYAML(data) {
131
140
  yaml += ` exports:\n`;
132
141
  info.exports.forEach(e => yaml += ` - "${e}"\n`);
133
142
  }
143
+ if (info.dependents && info.dependents.length > 0) {
144
+ yaml += ` dependents:\n`;
145
+ info.dependents.forEach(d => yaml += ` - "${d}"\n`);
146
+ }
134
147
  }
135
148
  return yaml;
136
149
  }
@@ -138,18 +151,15 @@ function generateYAML(data) {
138
151
  // ── Main Execution ────────────────────────────────────────────────────────────
139
152
  function main() {
140
153
  if (!fs.existsSync(AGENT_DIR)) {
141
- console.error('\x1b[31m✖ Error: .agent directory not found. Please run tribunal-kit init first.\x1b[0m');
154
+ console.error('\x1b[31m✖ Error: .agent directory not found.\x1b[0m');
142
155
  process.exit(1);
143
156
  }
144
157
 
145
- if (!fs.existsSync(HISTORY_DIR)) {
146
- fs.mkdirSync(HISTORY_DIR, { recursive: true });
147
- }
158
+ if (!fs.existsSync(HISTORY_DIR)) fs.mkdirSync(HISTORY_DIR, { recursive: true });
148
159
 
149
- // 1. Load incremental cache
150
160
  let cache = {};
151
161
  if (fs.existsSync(CACHE_FILE)) {
152
- try { cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); } catch(e) {}
162
+ try { cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); } catch { /* ignore */ }
153
163
  }
154
164
 
155
165
  console.log('\x1b[96m✦ Building Architecture Graph...\x1b[0m');
@@ -159,7 +169,6 @@ function main() {
159
169
  let parsedCount = 0;
160
170
  let cachedCount = 0;
161
171
 
162
- // 2. Parse or hit cache
163
172
  for (const file of files) {
164
173
  const stat = fs.statSync(file);
165
174
  const relativePath = path.relative(process.cwd(), file).replace(/\\/g, '/');
@@ -179,21 +188,126 @@ function main() {
179
188
  exports: parsed.exports
180
189
  };
181
190
  parsedCount++;
182
- } catch (err) {
183
- // Graceful fallback on unreadable files
184
- console.warn(`\x1b[33m ⚠ Skipping unreadable file: ${relativePath}\x1b[0m`);
191
+ } catch { /* ignore */ }
192
+ }
193
+ }
194
+
195
+ // Compute Dependents
196
+ for (const [_file, info] of Object.entries(graphData)) info.dependents = [];
197
+
198
+ const fileKeys = Object.keys(graphData);
199
+ for (const [file, info] of Object.entries(graphData)) {
200
+ for (const imp of info.imports) {
201
+ if (imp.startsWith('.')) {
202
+ let resolved = path.posix.join(path.dirname(file), imp);
203
+ // Look for direct match or .js / index.js
204
+ let matchingKey = fileKeys.find(k =>
205
+ k === resolved || k === resolved + '.js' || k === resolved + '.ts' || k === resolved + '/index.js'
206
+ );
207
+ if (matchingKey) {
208
+ if (!graphData[matchingKey].dependents.includes(file)) {
209
+ graphData[matchingKey].dependents.push(file);
210
+ }
211
+ }
185
212
  }
186
213
  }
187
214
  }
188
215
 
189
- // 3. Save states
216
+ // Compute Risk Score
217
+ function computeRisk(file) {
218
+ const visited = new Set();
219
+ function visit(node) {
220
+ if (visited.has(node)) return;
221
+ visited.add(node);
222
+ const deps = graphData[node]?.dependents || [];
223
+ deps.forEach(visit);
224
+ }
225
+ visit(file);
226
+ const radius = visited.size - 1;
227
+ let score = 'Low';
228
+ if (radius > 10) score = 'Critical';
229
+ else if (radius >= 5) score = 'High';
230
+ else if (radius >= 2) score = 'Medium';
231
+ return { score, count: Math.max(0, radius) };
232
+ }
233
+
234
+ for (const file of fileKeys) {
235
+ const risk = computeRisk(file);
236
+ graphData[file].riskScore = risk.score;
237
+ graphData[file].blastRadius = risk.count;
238
+
239
+ // Update cache with these values so visualizer can use it
240
+ if (cache[file]) {
241
+ cache[file].dependents = graphData[file].dependents;
242
+ cache[file].riskScore = risk.score;
243
+ cache[file].blastRadius = risk.count;
244
+ }
245
+ }
246
+
190
247
  fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
191
- const yamlOutput = generateYAML(graphData);
192
- fs.writeFileSync(GRAPH_FILE, yamlOutput);
248
+ fs.writeFileSync(GRAPH_FILE, generateYAML(graphData));
249
+
250
+ // ── Pre-Computed Context Snapshots (Option C) ───────────────────────────
251
+ const SNAPSHOTS_DIR = path.join(HISTORY_DIR, 'snapshots');
252
+ if (!fs.existsSync(SNAPSHOTS_DIR)) {
253
+ fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true });
254
+ } else {
255
+ // Clear stale snapshots
256
+ try {
257
+ const oldSnapshots = fs.readdirSync(SNAPSHOTS_DIR);
258
+ for (const f of oldSnapshots) fs.unlinkSync(path.join(SNAPSHOTS_DIR, f));
259
+ } catch { /* ignore */ }
260
+ }
261
+
262
+ console.log('\x1b[96m✦ Generating Context Snapshots...\x1b[0m');
263
+ for (const file of fileKeys) {
264
+ const info = graphData[file];
265
+ const snapshotFile = file.replace(/[\\/]/g, '__') + '.json';
266
+ const snapshotPath = path.join(SNAPSHOTS_DIR, snapshotFile);
267
+
268
+ let content = '';
269
+ try {
270
+ content = fs.readFileSync(path.join(process.cwd(), file), 'utf8');
271
+ } catch {
272
+ continue;
273
+ }
274
+
275
+ const snapshot = {
276
+ file: file,
277
+ riskScore: info.riskScore,
278
+ blastRadius: info.blastRadius,
279
+ imports: {},
280
+ dependents: info.dependents || [],
281
+ content: content
282
+ };
283
+
284
+ for (const imp of info.imports) {
285
+ if (imp.startsWith('.')) {
286
+ let resolved = path.posix.join(path.dirname(file), imp);
287
+ let matchingKey = fileKeys.find(k =>
288
+ k === resolved || k === resolved + '.js' || k === resolved + '.ts' || k === resolved + '/index.js'
289
+ );
290
+ if (matchingKey && graphData[matchingKey]) {
291
+ snapshot.imports[imp] = graphData[matchingKey].exports;
292
+ } else {
293
+ snapshot.imports[imp] = [];
294
+ }
295
+ } else {
296
+ snapshot.imports[imp] = [];
297
+ }
298
+ }
299
+
300
+ fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
301
+ }
302
+ console.log(` \x1b[2mSaved ${fileKeys.length} snapshots to: ${SNAPSHOTS_DIR}\x1b[0m`);
193
303
 
194
304
  console.log(`\n\x1b[32m✔ Graph successfully built.\x1b[0m`);
195
305
  console.log(` \x1b[2mParsed: ${parsedCount} files | Cached: ${cachedCount} files\x1b[0m`);
196
306
  console.log(` \x1b[2mSaved to: ${GRAPH_FILE}\x1b[0m`);
197
307
  }
308
+ // ── Exports (for testing & programmatic use) ─────────────────────────────────
309
+ module.exports = { parseFile, generateYAML, walkDir, isExcluded, main };
198
310
 
199
- main();
311
+ if (require.main === module) {
312
+ main();
313
+ }
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * graph_visualizer.js — Tribunal Kit Architecture Visualizer
4
+ * Reads the graph cache and generates a standalone HTML visualizer.
5
+ * Uses a native zero-dependency Canvas force-directed graph.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const AGENT_DIR = path.join(process.cwd(), '.agent');
14
+ const HISTORY_DIR = path.join(AGENT_DIR, 'history');
15
+ const CACHE_FILE = path.join(HISTORY_DIR, 'graph-cache.json');
16
+ const HTML_FILE = path.join(HISTORY_DIR, 'architecture-explorer.html');
17
+
18
+ function main() {
19
+ if (!fs.existsSync(CACHE_FILE)) {
20
+ console.error('\x1b[31m✖ Error: graph-cache.json not found. Run graph_builder.js first.\x1b[0m');
21
+ process.exit(1);
22
+ }
23
+
24
+ const cacheData = fs.readFileSync(CACHE_FILE, 'utf8');
25
+
26
+ const htmlContent = `<!DOCTYPE html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="UTF-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
+ <title>Tribunal Architecture Explorer</title>
32
+ <style>
33
+ :root {
34
+ --bg: #09090b;
35
+ --panel-bg: rgba(24, 24, 27, 0.8);
36
+ --border: #27272a;
37
+ --text: #e4e4e7;
38
+ --text-dim: #a1a1aa;
39
+ --critical: #ef4444;
40
+ --high: #f97316;
41
+ --medium: #eab308;
42
+ --low: #3b82f6;
43
+ --edge: rgba(255, 255, 255, 0.1);
44
+ }
45
+ body {
46
+ margin: 0;
47
+ padding: 0;
48
+ background: var(--bg);
49
+ color: var(--text);
50
+ font-family: system-ui, -apple-system, sans-serif;
51
+ overflow: hidden;
52
+ }
53
+ canvas {
54
+ display: block;
55
+ width: 100vw;
56
+ height: 100vh;
57
+ }
58
+ #ui-panel {
59
+ position: absolute;
60
+ top: 20px;
61
+ left: 20px;
62
+ width: 320px;
63
+ background: var(--panel-bg);
64
+ backdrop-filter: blur(12px);
65
+ border: 1px solid var(--border);
66
+ border-radius: 12px;
67
+ padding: 20px;
68
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
69
+ pointer-events: none; /* Let clicks pass to canvas if not on panel */
70
+ }
71
+ h1 { margin: 0 0 10px 0; font-size: 1.2rem; font-weight: 600; }
72
+ .stat { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 0.9rem; }
73
+ .stat .val { font-family: monospace; color: white; }
74
+ .legend { margin-top: 20px; display: grid; gap: 8px; font-size: 0.85rem; }
75
+ .legend-item { display: flex; align-items: center; gap: 8px; }
76
+ .dot { width: 10px; height: 10px; border-radius: 50%; }
77
+
78
+ #node-details {
79
+ margin-top: 20px;
80
+ padding-top: 20px;
81
+ border-top: 1px solid var(--border);
82
+ display: none;
83
+ pointer-events: auto;
84
+ }
85
+ #node-details h2 { margin: 0 0 10px 0; font-size: 1rem; word-break: break-all; }
86
+ .detail-row { font-size: 0.85rem; margin-bottom: 4px; color: var(--text-dim); }
87
+ .badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; color: black; }
88
+ ul { margin: 8px 0; padding-left: 20px; font-size: 0.85rem; color: var(--text-dim); max-height: 150px; overflow-y: auto; }
89
+ </style>
90
+ </head>
91
+ <body>
92
+
93
+ <canvas id="graph"></canvas>
94
+
95
+ <div id="ui-panel">
96
+ <h1>Tribunal Architecture</h1>
97
+ <div class="stat"><span>Nodes</span><span class="val" id="stat-nodes">0</span></div>
98
+ <div class="stat"><span>Edges</span><span class="val" id="stat-edges">0</span></div>
99
+
100
+ <div class="legend">
101
+ <div class="legend-item"><div class="dot" style="background: var(--critical)"></div> Critical (>10 dependents)</div>
102
+ <div class="legend-item"><div class="dot" style="background: var(--high)"></div> High (5-10 dependents)</div>
103
+ <div class="legend-item"><div class="dot" style="background: var(--medium)"></div> Medium (2-4 dependents)</div>
104
+ <div class="legend-item"><div class="dot" style="background: var(--low)"></div> Low (0-1 dependents)</div>
105
+ </div>
106
+
107
+ <div id="node-details">
108
+ <h2 id="nd-title">file.js</h2>
109
+ <div class="detail-row">Risk Score: <span id="nd-risk" class="badge">Low</span></div>
110
+ <div class="detail-row">Blast Radius: <span id="nd-blast" style="color:white;font-weight:bold">0</span> files</div>
111
+ <div class="detail-row" style="margin-top:10px">Imports:</div>
112
+ <ul id="nd-imports"></ul>
113
+ <div class="detail-row">Dependents:</div>
114
+ <ul id="nd-dependents"></ul>
115
+ </div>
116
+ </div>
117
+
118
+ <script>
119
+ const rawData = JSON.parse(decodeURIComponent("${encodeURIComponent(cacheData)}"));
120
+
121
+ const nodes = [];
122
+ const edges = [];
123
+ const nodeMap = new Map();
124
+
125
+ // Build Nodes
126
+ Object.keys(rawData).forEach(file => {
127
+ const info = rawData[file];
128
+ const node = {
129
+ id: file,
130
+ imports: info.imports || [],
131
+ dependents: info.dependents || [],
132
+ riskScore: info.riskScore || 'Low',
133
+ blastRadius: info.blastRadius || 0,
134
+ x: Math.random() * window.innerWidth,
135
+ y: Math.random() * window.innerHeight,
136
+ vx: 0,
137
+ vy: 0,
138
+ radius: Math.min(20, Math.max(5, 5 + (info.blastRadius * 1.5)))
139
+ };
140
+
141
+ if (node.riskScore === 'Critical') node.color = '#ef4444';
142
+ else if (node.riskScore === 'High') node.color = '#f97316';
143
+ else if (node.riskScore === 'Medium') node.color = '#eab308';
144
+ else node.color = '#3b82f6';
145
+
146
+ nodes.push(node);
147
+ nodeMap.set(file, node);
148
+ });
149
+
150
+ // Build Edges
151
+ nodes.forEach(node => {
152
+ node.imports.forEach(imp => {
153
+ if (imp.startsWith('.')) {
154
+ // Try to resolve
155
+ const dir = node.id.split('/').slice(0, -1).join('/');
156
+ let resolved = dir ? dir + '/' + imp.replace('./', '') : imp.replace('./', '');
157
+
158
+ // Normalize standard paths relative to array
159
+ let target = nodes.find(n => n.id === resolved || n.id === resolved + '.js' || n.id === resolved + '.ts');
160
+ if (target) {
161
+ edges.push({ source: node, target: target });
162
+ }
163
+ }
164
+ });
165
+ });
166
+
167
+ document.getElementById('stat-nodes').innerText = nodes.length;
168
+ document.getElementById('stat-edges').innerText = edges.length;
169
+
170
+ // Force Directed Graph Simulation
171
+ const canvas = document.getElementById('graph');
172
+ const ctx = canvas.getContext('2d');
173
+
174
+ function resize() {
175
+ canvas.width = window.innerWidth;
176
+ canvas.height = window.innerHeight;
177
+ }
178
+ window.addEventListener('resize', resize);
179
+ resize();
180
+
181
+ let hoveredNode = null;
182
+ let selectedNode = null;
183
+ let isDragging = false;
184
+
185
+ // Physics constants
186
+ const REPULSION = 2000;
187
+ const SPRING_LENGTH = 100;
188
+ const SPRING_STRENGTH = 0.05;
189
+ const DAMPING = 0.85;
190
+
191
+ function simulate() {
192
+ // Repulsion
193
+ for (let i = 0; i < nodes.length; i++) {
194
+ for (let j = i + 1; j < nodes.length; j++) {
195
+ const n1 = nodes[i];
196
+ const n2 = nodes[j];
197
+ const dx = n2.x - n1.x;
198
+ const dy = n2.y - n1.y;
199
+ let dist = Math.sqrt(dx*dx + dy*dy) || 1;
200
+ if (dist < 300) {
201
+ const force = REPULSION / (dist * dist);
202
+ const fx = (dx / dist) * force;
203
+ const fy = (dy / dist) * force;
204
+ n1.vx -= fx;
205
+ n1.vy -= fy;
206
+ n2.vx += fx;
207
+ n2.vy += fy;
208
+ }
209
+ }
210
+ }
211
+
212
+ // Attraction (Springs)
213
+ edges.forEach(edge => {
214
+ const dx = edge.target.x - edge.source.x;
215
+ const dy = edge.target.y - edge.source.y;
216
+ const dist = Math.sqrt(dx*dx + dy*dy) || 1;
217
+ const force = (dist - SPRING_LENGTH) * SPRING_STRENGTH;
218
+ const fx = (dx / dist) * force;
219
+ const fy = (dy / dist) * force;
220
+
221
+ edge.source.vx += fx;
222
+ edge.source.vy += fy;
223
+ edge.target.vx -= fx;
224
+ edge.target.vy -= fy;
225
+ });
226
+
227
+ // Center gravity
228
+ const cx = canvas.width / 2;
229
+ const cy = canvas.height / 2;
230
+ nodes.forEach(n => {
231
+ n.vx += (cx - n.x) * 0.01;
232
+ n.vy += (cy - n.y) * 0.01;
233
+ });
234
+
235
+ // Update positions
236
+ nodes.forEach(n => {
237
+ if (n === selectedNode && isDragging) return; // don't move dragged node
238
+ n.vx *= DAMPING;
239
+ n.vy *= DAMPING;
240
+ n.x += n.vx;
241
+ n.y += n.vy;
242
+ });
243
+ }
244
+
245
+ function draw() {
246
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
247
+
248
+ // Draw edges
249
+ ctx.lineWidth = 1;
250
+ edges.forEach(edge => {
251
+ let isHighlighted = false;
252
+ if (hoveredNode) {
253
+ isHighlighted = (edge.source === hoveredNode || edge.target === hoveredNode);
254
+ } else if (selectedNode) {
255
+ isHighlighted = (edge.source === selectedNode || edge.target === selectedNode);
256
+ }
257
+
258
+ if (hoveredNode || selectedNode) {
259
+ ctx.strokeStyle = isHighlighted ? 'rgba(255,255,255,0.8)' : 'rgba(255,255,255,0.05)';
260
+ ctx.lineWidth = isHighlighted ? 2 : 1;
261
+ } else {
262
+ ctx.strokeStyle = 'rgba(255,255,255,0.15)';
263
+ ctx.lineWidth = 1;
264
+ }
265
+
266
+ ctx.beginPath();
267
+ ctx.moveTo(edge.source.x, edge.source.y);
268
+ ctx.lineTo(edge.target.x, edge.target.y);
269
+ ctx.stroke();
270
+ });
271
+
272
+ // Draw nodes
273
+ nodes.forEach(n => {
274
+ let opacity = 1;
275
+ if (hoveredNode && n !== hoveredNode && !edges.some(e => (e.source===hoveredNode && e.target===n) || (e.target===hoveredNode && e.source===n))) {
276
+ opacity = 0.2;
277
+ } else if (selectedNode && !hoveredNode && n !== selectedNode && !edges.some(e => (e.source===selectedNode && e.target===n) || (e.target===selectedNode && e.source===n))) {
278
+ opacity = 0.2;
279
+ }
280
+
281
+ ctx.beginPath();
282
+ ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2);
283
+ ctx.fillStyle = n.color;
284
+ ctx.globalAlpha = opacity;
285
+ ctx.fill();
286
+
287
+ if (n === hoveredNode || n === selectedNode) {
288
+ ctx.strokeStyle = '#fff';
289
+ ctx.lineWidth = 2;
290
+ ctx.stroke();
291
+
292
+ ctx.globalAlpha = 1;
293
+ ctx.fillStyle = '#fff';
294
+ ctx.font = '12px system-ui';
295
+ ctx.fillText(n.id.split('/').pop(), n.x + n.radius + 5, n.y + 4);
296
+ }
297
+ ctx.globalAlpha = 1;
298
+ });
299
+ }
300
+
301
+ function loop() {
302
+ simulate();
303
+ draw();
304
+ requestAnimationFrame(loop);
305
+ }
306
+ loop();
307
+
308
+ // Interaction
309
+ canvas.addEventListener('mousemove', e => {
310
+ const rect = canvas.getBoundingClientRect();
311
+ const mx = e.clientX - rect.left;
312
+ const my = e.clientY - rect.top;
313
+
314
+ if (isDragging && selectedNode) {
315
+ selectedNode.x = mx;
316
+ selectedNode.y = my;
317
+ return;
318
+ }
319
+
320
+ hoveredNode = null;
321
+ for (let i = nodes.length - 1; i >= 0; i--) {
322
+ const n = nodes[i];
323
+ const dx = mx - n.x;
324
+ const dy = my - n.y;
325
+ if (dx*dx + dy*dy < (n.radius + 5)**2) {
326
+ hoveredNode = n;
327
+ break;
328
+ }
329
+ }
330
+ canvas.style.cursor = hoveredNode ? 'pointer' : 'default';
331
+ });
332
+
333
+ canvas.addEventListener('mousedown', e => {
334
+ if (hoveredNode) {
335
+ selectedNode = hoveredNode;
336
+ isDragging = true;
337
+ showDetails(selectedNode);
338
+ } else {
339
+ selectedNode = null;
340
+ document.getElementById('node-details').style.display = 'none';
341
+ }
342
+ });
343
+
344
+ canvas.addEventListener('mouseup', () => { isDragging = false; });
345
+
346
+ function showDetails(n) {
347
+ const panel = document.getElementById('node-details');
348
+ panel.style.display = 'block';
349
+ document.getElementById('nd-title').innerText = n.id;
350
+
351
+ const riskEl = document.getElementById('nd-risk');
352
+ riskEl.innerText = n.riskScore;
353
+ riskEl.style.backgroundColor = n.color;
354
+
355
+ document.getElementById('nd-blast').innerText = n.blastRadius;
356
+
357
+ const importsUl = document.getElementById('nd-imports');
358
+ importsUl.innerHTML = '';
359
+ n.imports.forEach(i => {
360
+ const li = document.createElement('li');
361
+ li.innerText = i;
362
+ importsUl.appendChild(li);
363
+ });
364
+ if(n.imports.length===0) importsUl.innerHTML = '<li>None</li>';
365
+
366
+ const depsUl = document.getElementById('nd-dependents');
367
+ depsUl.innerHTML = '';
368
+ n.dependents.forEach(d => {
369
+ const li = document.createElement('li');
370
+ li.innerText = d;
371
+ depsUl.appendChild(li);
372
+ });
373
+ if(n.dependents.length===0) depsUl.innerHTML = '<li>None</li>';
374
+ }
375
+ </script>
376
+ </body>
377
+ </html>`;
378
+
379
+ fs.writeFileSync(HTML_FILE, htmlContent);
380
+ console.log(`\x1b[32m✔ Interactive visualizer generated.\x1b[0m`);
381
+ console.log(` \x1b[2mSaved to: ${HTML_FILE}\x1b[0m`);
382
+ }
383
+
384
+ main();