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.
- package/.agent/agents/api-architect.md +66 -66
- package/.agent/agents/db-latency-auditor.md +216 -216
- package/.agent/agents/precedence-reviewer.md +250 -250
- package/.agent/agents/resilience-reviewer.md +88 -88
- package/.agent/agents/schema-reviewer.md +67 -67
- package/.agent/agents/throughput-optimizer.md +299 -299
- package/.agent/agents/ui-ux-auditor.md +292 -292
- package/.agent/agents/vitals-reviewer.md +223 -223
- package/.agent/scripts/_colors.js +18 -18
- package/.agent/scripts/_utils.js +42 -42
- package/.agent/scripts/append_flow.js +72 -72
- package/.agent/scripts/auto_preview.js +197 -197
- package/.agent/scripts/bundle_analyzer.js +290 -290
- package/.agent/scripts/case_law_manager.js +17 -6
- package/.agent/scripts/checklist.js +266 -266
- package/.agent/scripts/colors.js +17 -17
- package/.agent/scripts/compress_skills.js +141 -141
- package/.agent/scripts/consolidate_skills.js +149 -149
- package/.agent/scripts/context_broker.js +611 -609
- package/.agent/scripts/deep_compress.js +150 -150
- package/.agent/scripts/dependency_analyzer.js +272 -272
- package/.agent/scripts/graph_builder.js +151 -37
- package/.agent/scripts/graph_visualizer.js +384 -0
- package/.agent/scripts/inner_loop_validator.js +451 -465
- package/.agent/scripts/lint_runner.js +187 -187
- package/.agent/scripts/minify_context.js +100 -100
- package/.agent/scripts/mutation_runner.js +280 -0
- package/.agent/scripts/patch_skills_meta.js +156 -156
- package/.agent/scripts/patch_skills_output.js +244 -244
- package/.agent/scripts/schema_validator.js +297 -297
- package/.agent/scripts/security_scan.js +303 -303
- package/.agent/scripts/session_manager.js +276 -276
- package/.agent/scripts/skill_evolution.js +644 -644
- package/.agent/scripts/skill_integrator.js +313 -313
- package/.agent/scripts/strengthen_skills.js +193 -193
- package/.agent/scripts/strip_tribunal.js +47 -47
- package/.agent/scripts/swarm_dispatcher.js +360 -360
- package/.agent/scripts/test_runner.js +193 -193
- package/.agent/scripts/utils.js +32 -32
- package/.agent/scripts/verify_all.js +257 -256
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +1 -1
- package/.agent/skills/doc.md +1 -1
- package/.agent/skills/knowledge-graph/SKILL.md +32 -16
- package/.agent/skills/testing-patterns/SKILL.md +19 -2
- package/.agent/skills/ui-ux-pro-max/SKILL.md +480 -43
- package/.agent/workflows/generate.md +183 -183
- package/.agent/workflows/tribunal-speed.md +183 -183
- package/README.md +1 -1
- package/bin/tribunal-kit.js +134 -17
- package/package.json +6 -3
- package/scripts/changelog.js +167 -167
- package/scripts/sync-version.js +81 -81
- package/.agent/scripts/__pycache__/_colors.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/_utils.cpython-311.pyc +0 -0
- 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 (
|
|
59
|
-
return fileList;
|
|
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
|
-
//
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
102
|
+
const defaultExportRegex = /^[\s]*export\s+default\s+([a-zA-Z0-9_]+)/gm;
|
|
95
103
|
|
|
96
104
|
let match;
|
|
97
|
-
while ((match =
|
|
98
|
-
while ((match = requireRegex.exec(
|
|
99
|
-
while ((match = dynamicImportRegex.exec(
|
|
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(
|
|
102
|
-
while ((match = defaultExportRegex.exec(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
192
|
-
|
|
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();
|