tribunal-kit 5.7.0 → 5.8.0

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 (56) hide show
  1. package/.agent/ARCHITECTURE.md +6 -7
  2. package/.agent/agents/frontend-reviewer.md +13 -0
  3. package/.agent/agents/frontend-specialist.md +14 -0
  4. package/.agent/agents/logic-reviewer.md +11 -0
  5. package/.agent/agents/orchestrator.md +15 -0
  6. package/.agent/agents/security-auditor.md +13 -0
  7. package/.agent/agents/ui-ux-auditor.md +7 -31
  8. package/.agent/history/memory/.memory.idx +766 -0
  9. package/.agent/history/memory/MEMORY.md +62 -0
  10. package/.agent/routing_index.json +694 -714
  11. package/.agent/rules/GEMINI.md +58 -8
  12. package/.agent/scripts/_colors.js +131 -89
  13. package/.agent/scripts/_utils.js +163 -128
  14. package/.agent/scripts/auto_preview.js +207 -197
  15. package/.agent/scripts/bundle_analyzer.js +227 -192
  16. package/.agent/scripts/case_law_manager.js +991 -689
  17. package/.agent/scripts/checklist.js +233 -190
  18. package/.agent/scripts/context_broker.js +930 -605
  19. package/.agent/scripts/dependency_analyzer.js +275 -184
  20. package/.agent/scripts/graph_builder.js +412 -341
  21. package/.agent/scripts/graph_visualizer.js +392 -390
  22. package/.agent/scripts/graph_zoom.js +198 -156
  23. package/.agent/scripts/inner_loop_validator.js +523 -445
  24. package/.agent/scripts/lint_runner.js +199 -157
  25. package/.agent/scripts/marathon_harness.js +819 -661
  26. package/.agent/scripts/minify_context.js +115 -100
  27. package/.agent/scripts/mutation_runner.js +321 -280
  28. package/.agent/scripts/prompt_compiler.js +62 -42
  29. package/.agent/scripts/schema_validator.js +373 -280
  30. package/.agent/scripts/security_scan.js +333 -190
  31. package/.agent/scripts/session_manager.js +306 -270
  32. package/.agent/scripts/skill_evolution.js +810 -637
  33. package/.agent/scripts/skill_integrator.js +327 -307
  34. package/.agent/scripts/strengthen_skills.js +203 -193
  35. package/.agent/scripts/swarm_dispatcher.js +558 -457
  36. package/.agent/scripts/test_runner.js +178 -152
  37. package/.agent/scripts/verify_all.js +200 -168
  38. package/.agent/skills/fabel-protocol/SKILL.md +235 -0
  39. package/.agent/skills/thinking-protocol/SKILL.md +27 -0
  40. package/.agent/workflows/generate.md +1 -1
  41. package/.agent/workflows/tribunal-speed.md +1 -1
  42. package/README.md +53 -53
  43. package/bin/mcp-server.js +460 -175
  44. package/bin/tribunal-kit.js +1245 -987
  45. package/bin/wrapper.js +104 -74
  46. package/dist/cli.js +31 -0
  47. package/dist/commands/case.js +23 -0
  48. package/dist/commands/compile.js +84 -0
  49. package/dist/commands/init.js +42 -0
  50. package/dist/commands/learn.js +57 -0
  51. package/dist/commands/memory.js +456 -0
  52. package/package.json +2 -2
  53. package/scripts/benchmark.js +162 -125
  54. package/scripts/changelog.js +196 -168
  55. package/scripts/sync-version.js +94 -81
  56. package/scripts/validate-payload.js +85 -78
@@ -1,341 +1,412 @@
1
- #!/usr/bin/env node
2
- /**
3
- * graph_builder.js — Tribunal Kit Macro Graph Mapper
4
- * Parses project structure for imports, exports, and dependencies
5
- * using incremental caching and zero external dependencies.
6
- * Now includes Blast Radius calculation and robust token stripping.
7
- */
8
-
9
- 'use strict';
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const crypto = require('crypto');
14
-
15
- const { RED, GREEN, BOLD, DIM, CYAN, RESET, timer, formatMs } = require('./_colors');
16
-
17
- const AGENT_DIR = path.join(process.cwd(), '.agent');
18
- const HISTORY_DIR = path.join(AGENT_DIR, 'history');
19
- const CACHE_FILE = path.join(HISTORY_DIR, 'graph-cache.json');
20
- const GRAPH_FILE = path.join(HISTORY_DIR, 'architecture-graph.yaml');
21
-
22
- // ── Exclusions & Safety ───────────────────────────────────────────────────────
23
- const DEFAULT_EXCLUSIONS = new Set([
24
- 'node_modules', '.git', '.next', 'dist', 'build', 'coverage', '.agent', 'artifacts'
25
- ]);
26
-
27
- function loadGitIgnore() {
28
- const gitignorePath = path.join(process.cwd(), '.gitignore');
29
- if (!fs.existsSync(gitignorePath)) return [];
30
-
31
- return fs.readFileSync(gitignorePath, 'utf8')
32
- .split('\n')
33
- .map(line => line.trim())
34
- .filter(line => line && !line.startsWith('#'))
35
- .map(line => line.replace(/\/$/, '').replace(/^\//, ''));
36
- }
37
-
38
- const customExclusions = loadGitIgnore();
39
-
40
- function isExcluded(filePath) {
41
- const parts = filePath.split(path.sep);
42
- if (parts.some(p => DEFAULT_EXCLUSIONS.has(p))) return true;
43
-
44
- const relativePath = path.relative(process.cwd(), filePath).replace(/\\/g, '/');
45
- for (const pattern of customExclusions) {
46
- if (relativePath.includes(pattern)) return true;
47
- }
48
- return false;
49
- }
50
-
51
- // ── Content Hashing ───────────────────────────────────────────────────────────
52
- function getFileHash(filePath) {
53
- const content = fs.readFileSync(filePath);
54
- return crypto.createHash('sha1').update(content).digest('hex');
55
- }
56
-
57
- // ── Traversal ─────────────────────────────────────────────────────────────────
58
- function walkDir(dir, fileList = []) {
59
- if (!fs.existsSync(dir) || isExcluded(dir)) return fileList;
60
-
61
- let files;
62
- try {
63
- files = fs.readdirSync(dir);
64
- } catch (_err) {
65
- return fileList;
66
- }
67
-
68
- for (const file of files) {
69
- const filePath = path.join(dir, file);
70
- if (isExcluded(filePath)) continue;
71
-
72
- if (fs.statSync(filePath).isDirectory()) {
73
- walkDir(filePath, fileList);
74
- } else {
75
- if (/\.(js|jsx|ts|tsx|mjs|cjs)$/.test(file)) {
76
- fileList.push(filePath);
77
- }
78
- }
79
- }
80
- return fileList;
81
- }
82
-
83
- // ── Regex AST Extraction ──────────────────────────────────────────────────────
84
- function parseFile(content) {
85
- const imports = new Set();
86
- const exports = new Set();
87
-
88
- // Parse from semi-cleaned content (comments removed)
89
- // WAIT: If I stripped strings, how do I get the import path?
90
- // The previous implementation used strings `['"]([^'"]+)['"]`.
91
- // If I strip strings, the import path is lost!
92
- // Let's rollback that logic or adapt it.
93
- // Instead of stripping all strings, we should only strip strings if they are NOT following 'import ' or 'require('
94
- // To do this simply, let's keep strings, but just be careful.
95
- // Actually, string literals inside `require("...")` are what we want.
96
- // So `parseFile` should probably NOT strip strings, but just use a safer regex.
97
- // The false positive in `dependency_analyzer` was because of `const diff = "import a from 'a'"`.
98
- // Let's use `stripStringsAndComments` but we DO NOT strip strings.
99
- // We only strip comments.
100
-
101
- // I'll define an inner function to just strip comments to be safe for imports.
102
- // Let's stick to the simple `.replace` for comments for now, and rely on regex boundaries.
103
- const semiCleanContent = content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
104
-
105
- const importRegex2 = /^[\s]*import(?:(?:[\w*\s{},]*)\sfrom\s+)?['"]([^'"]+)['"]/gm;
106
- const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
107
- const dynamicImportRegex = /import\(['"]([^'"]+)['"]\)/g;
108
-
109
- const exportRegex = /^[\s]*export\s+(?:const|let|var|function|class)\s+([a-zA-Z0-9_]+)/gm;
110
- const moduleExportRegex = /module\.exports\s*=\s*\{([^}]+)\}/g;
111
- const defaultExportRegex = /^[\s]*export\s+default\s+([a-zA-Z0-9_]+)/gm;
112
-
113
- let match;
114
- while ((match = importRegex2.exec(semiCleanContent)) !== null) imports.add(match[1]);
115
- while ((match = requireRegex.exec(semiCleanContent)) !== null) imports.add(match[1]);
116
- while ((match = dynamicImportRegex.exec(semiCleanContent)) !== null) imports.add(match[1]);
117
-
118
- while ((match = exportRegex.exec(semiCleanContent)) !== null) exports.add(match[1]);
119
- while ((match = defaultExportRegex.exec(semiCleanContent)) !== null) exports.add(match[1]);
120
-
121
- while ((match = moduleExportRegex.exec(semiCleanContent)) !== null) {
122
- const tokens = match[1].split(',').map(s => s.trim().split(':')[0].trim());
123
- tokens.forEach(t => t && exports.add(t));
124
- }
125
-
126
- return {
127
- imports: Array.from(imports),
128
- exports: Array.from(exports)
129
- };
130
- }
131
-
132
- // ── YAML Generation ───────────────────────────────────────────────────────────
133
- function generateYAML(data) {
134
- let yaml = '# Auto-generated Architecture Graph by Tribunal Kit\n';
135
- yaml += '# DO NOT EDIT MANUALLY - Auto-updates via incremental cache\n\n';
136
-
137
- for (const [file, info] of Object.entries(data)) {
138
- if (info.imports.length === 0 && info.exports.length === 0 && (!info.dependents || info.dependents.length === 0)) continue;
139
-
140
- yaml += `"${file}":\n`;
141
- yaml += ` riskScore: "${info.riskScore || 'Low'}"\n`;
142
- yaml += ` blastRadius: ${info.blastRadius || 0}\n`;
143
-
144
- if (info.imports && info.imports.length > 0) {
145
- yaml += ` imports:\n`;
146
- info.imports.forEach(i => yaml += ` - "${i}"\n`);
147
- }
148
- if (info.exports && info.exports.length > 0) {
149
- yaml += ` exports:\n`;
150
- info.exports.forEach(e => yaml += ` - "${e}"\n`);
151
- }
152
- if (info.dependents && info.dependents.length > 0) {
153
- yaml += ` dependents:\n`;
154
- info.dependents.forEach(d => yaml += ` - "${d}"\n`);
155
- }
156
- }
157
- return yaml;
158
- }
159
-
160
- // ── Main Execution ────────────────────────────────────────────────────────────
161
- function main() {
162
- if (!fs.existsSync(AGENT_DIR)) {
163
- console.error(`${RED}✖ Error: .agent directory not found.${RESET}`);
164
- process.exit(1);
165
- }
166
-
167
- if (!fs.existsSync(HISTORY_DIR)) fs.mkdirSync(HISTORY_DIR, { recursive: true });
168
-
169
- let cache = {};
170
- if (fs.existsSync(CACHE_FILE)) {
171
- try { cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); } catch { /* ignore */ }
172
- }
173
-
174
- const totalTimer = timer();
175
- console.log(`${CYAN}✦ Building Architecture Graph...${RESET}`);
176
- const files = walkDir(process.cwd());
177
- const graphData = {};
178
-
179
- let parsedCount = 0;
180
- let cachedCount = 0;
181
- const changedFiles = new Set();
182
-
183
- for (const file of files) {
184
- const relativePath = path.relative(process.cwd(), file).replace(/\\/g, '/');
185
- let fileHash;
186
- try {
187
- fileHash = getFileHash(file);
188
- } catch { continue; }
189
-
190
- if (cache[relativePath] && cache[relativePath].hash === fileHash) {
191
- graphData[relativePath] = { imports: cache[relativePath].imports, exports: cache[relativePath].exports };
192
- cachedCount++;
193
- } else {
194
- try {
195
- const content = fs.readFileSync(file, 'utf8');
196
- const parsed = parseFile(content);
197
- graphData[relativePath] = parsed;
198
-
199
- cache[relativePath] = {
200
- hash: fileHash,
201
- imports: parsed.imports,
202
- exports: parsed.exports
203
- };
204
- parsedCount++;
205
- changedFiles.add(relativePath);
206
- } catch { /* ignore */ }
207
- }
208
- }
209
-
210
- // Compute Dependents
211
- for (const [_file, info] of Object.entries(graphData)) info.dependents = [];
212
-
213
- const fileKeys = Object.keys(graphData);
214
- for (const [file, info] of Object.entries(graphData)) {
215
- for (const imp of info.imports) {
216
- if (imp.startsWith('.')) {
217
- let resolved = path.posix.join(path.dirname(file), imp);
218
- // Look for direct match or .js / index.js
219
- let matchingKey = fileKeys.find(k =>
220
- k === resolved || k === resolved + '.js' || k === resolved + '.ts' || k === resolved + '/index.js'
221
- );
222
- if (matchingKey) {
223
- if (!graphData[matchingKey].dependents.includes(file)) {
224
- graphData[matchingKey].dependents.push(file);
225
- }
226
- }
227
- }
228
- }
229
- }
230
-
231
- // Compute Risk Score
232
- function computeRisk(file) {
233
- const visited = new Set();
234
- function visit(node) {
235
- if (visited.has(node)) return;
236
- visited.add(node);
237
- const deps = graphData[node]?.dependents || [];
238
- deps.forEach(visit);
239
- }
240
- visit(file);
241
- const radius = visited.size - 1;
242
- let score = 'Low';
243
- if (radius > 10) score = 'Critical';
244
- else if (radius >= 5) score = 'High';
245
- else if (radius >= 2) score = 'Medium';
246
- return { score, count: Math.max(0, radius) };
247
- }
248
-
249
- for (const file of fileKeys) {
250
- const risk = computeRisk(file);
251
- graphData[file].riskScore = risk.score;
252
- graphData[file].blastRadius = risk.count;
253
-
254
- // Update cache with these values so visualizer can use it
255
- if (cache[file]) {
256
- cache[file].dependents = graphData[file].dependents;
257
- cache[file].riskScore = risk.score;
258
- cache[file].blastRadius = risk.count;
259
- }
260
- }
261
-
262
- fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
263
- fs.writeFileSync(GRAPH_FILE, generateYAML(graphData));
264
-
265
- // ── Pre-Computed Context Snapshots (Incremental) ─────────────────────────
266
- const SNAPSHOTS_DIR = path.join(HISTORY_DIR, 'snapshots');
267
- if (!fs.existsSync(SNAPSHOTS_DIR)) {
268
- fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true });
269
- }
270
-
271
- // Clean up snapshots for files that no longer exist
272
- try {
273
- const existingSnapshots = fs.readdirSync(SNAPSHOTS_DIR);
274
- const currentFileSet = new Set(fileKeys.map(f => f.replace(/[\\/]/g, '__') + '.json'));
275
- for (const snap of existingSnapshots) {
276
- if (!currentFileSet.has(snap)) fs.unlinkSync(path.join(SNAPSHOTS_DIR, snap));
277
- }
278
- } catch { /* ignore */ }
279
-
280
- console.log(`${CYAN}✦ Generating Context Snapshots...${RESET}`);
281
- let snapshotWritten = 0;
282
- let snapshotSkipped = 0;
283
- for (const file of fileKeys) {
284
- const info = graphData[file];
285
- const snapshotFile = file.replace(/[\\/]/g, '__') + '.json';
286
- const snapshotPath = path.join(SNAPSHOTS_DIR, snapshotFile);
287
-
288
- // Skip unchanged files that already have a snapshot
289
- if (!changedFiles.has(file) && fs.existsSync(snapshotPath)) {
290
- snapshotSkipped++;
291
- continue;
292
- }
293
-
294
- let content = '';
295
- try {
296
- content = fs.readFileSync(path.join(process.cwd(), file), 'utf8');
297
- } catch {
298
- continue;
299
- }
300
-
301
- const snapshot = {
302
- file: file,
303
- riskScore: info.riskScore,
304
- blastRadius: info.blastRadius,
305
- imports: {},
306
- dependents: info.dependents || [],
307
- content: content
308
- };
309
-
310
- for (const imp of info.imports) {
311
- if (imp.startsWith('.')) {
312
- let resolved = path.posix.join(path.dirname(file), imp);
313
- let matchingKey = fileKeys.find(k =>
314
- k === resolved || k === resolved + '.js' || k === resolved + '.ts' || k === resolved + '/index.js'
315
- );
316
- if (matchingKey && graphData[matchingKey]) {
317
- snapshot.imports[imp] = graphData[matchingKey].exports;
318
- } else {
319
- snapshot.imports[imp] = [];
320
- }
321
- } else {
322
- snapshot.imports[imp] = [];
323
- }
324
- }
325
-
326
- fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
327
- snapshotWritten++;
328
- }
329
- console.log(` ${DIM}Snapshots: ${snapshotWritten} written | ${snapshotSkipped} cached${RESET}`);
330
-
331
- const totalMs = totalTimer();
332
- console.log(`\n${GREEN}${BOLD}✔ Graph successfully built.${RESET}`);
333
- console.log(` ${DIM}Parsed: ${parsedCount} files | Cached: ${cachedCount} files | ${formatMs(totalMs)}${RESET}`);
334
- console.log(` ${DIM}Saved to: ${GRAPH_FILE}${RESET}`);
335
- }
336
- // ── Exports (for testing & programmatic use) ─────────────────────────────────
337
- module.exports = { parseFile, generateYAML, walkDir, isExcluded, getFileHash, main };
338
-
339
- if (require.main === module) {
340
- main();
341
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * graph_builder.js — Tribunal Kit Macro Graph Mapper
4
+ * Parses project structure for imports, exports, and dependencies
5
+ * using incremental caching and zero external dependencies.
6
+ * Now includes Blast Radius calculation and robust token stripping.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const crypto = require("crypto");
14
+
15
+ const {
16
+ RED,
17
+ GREEN,
18
+ BOLD,
19
+ DIM,
20
+ CYAN,
21
+ RESET,
22
+ timer,
23
+ formatMs,
24
+ } = require("./_colors");
25
+
26
+ const AGENT_DIR = path.join(process.cwd(), ".agent");
27
+ const HISTORY_DIR = path.join(AGENT_DIR, "history");
28
+ const CACHE_FILE = path.join(HISTORY_DIR, "graph-cache.json");
29
+ const GRAPH_FILE = path.join(HISTORY_DIR, "architecture-graph.yaml");
30
+
31
+ // ── Exclusions & Safety ───────────────────────────────────────────────────────
32
+ const DEFAULT_EXCLUSIONS = new Set([
33
+ "node_modules",
34
+ ".git",
35
+ ".next",
36
+ "dist",
37
+ "build",
38
+ "coverage",
39
+ ".agent",
40
+ "artifacts",
41
+ ]);
42
+
43
+ function loadGitIgnore() {
44
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
45
+ if (!fs.existsSync(gitignorePath)) return [];
46
+
47
+ return fs
48
+ .readFileSync(gitignorePath, "utf8")
49
+ .split("\n")
50
+ .map((line) => line.trim())
51
+ .filter((line) => line && !line.startsWith("#"))
52
+ .map((line) => line.replace(/\/$/, "").replace(/^\//, ""));
53
+ }
54
+
55
+ const customExclusions = loadGitIgnore();
56
+
57
+ function isExcluded(filePath) {
58
+ const parts = filePath.split(path.sep);
59
+ if (parts.some((p) => DEFAULT_EXCLUSIONS.has(p))) return true;
60
+
61
+ const relativePath = path
62
+ .relative(process.cwd(), filePath)
63
+ .replace(/\\/g, "/");
64
+ for (const pattern of customExclusions) {
65
+ if (relativePath.includes(pattern)) return true;
66
+ }
67
+ return false;
68
+ }
69
+
70
+ // ── Content Hashing ───────────────────────────────────────────────────────────
71
+ function getFileHash(filePath) {
72
+ const content = fs.readFileSync(filePath);
73
+ return crypto.createHash("sha1").update(content).digest("hex");
74
+ }
75
+
76
+ // ── Traversal ─────────────────────────────────────────────────────────────────
77
+ function walkDir(dir, fileList = []) {
78
+ if (!fs.existsSync(dir) || isExcluded(dir)) return fileList;
79
+
80
+ let files;
81
+ try {
82
+ files = fs.readdirSync(dir);
83
+ } catch (_err) {
84
+ return fileList;
85
+ }
86
+
87
+ for (const file of files) {
88
+ const filePath = path.join(dir, file);
89
+ if (isExcluded(filePath)) continue;
90
+
91
+ if (fs.statSync(filePath).isDirectory()) {
92
+ walkDir(filePath, fileList);
93
+ } else {
94
+ if (/\.(js|jsx|ts|tsx|mjs|cjs)$/.test(file)) {
95
+ fileList.push(filePath);
96
+ }
97
+ }
98
+ }
99
+ return fileList;
100
+ }
101
+
102
+ // ── Regex AST Extraction ──────────────────────────────────────────────────────
103
+ function parseFile(content) {
104
+ const imports = new Set();
105
+ const exports = new Set();
106
+
107
+ // Parse from semi-cleaned content (comments removed)
108
+ // WAIT: If I stripped strings, how do I get the import path?
109
+ // The previous implementation used strings `['"]([^'"]+)['"]`.
110
+ // If I strip strings, the import path is lost!
111
+ // Let's rollback that logic or adapt it.
112
+ // Instead of stripping all strings, we should only strip strings if they are NOT following 'import ' or 'require('
113
+ // To do this simply, let's keep strings, but just be careful.
114
+ // Actually, string literals inside `require("...")` are what we want.
115
+ // So `parseFile` should probably NOT strip strings, but just use a safer regex.
116
+ // The false positive in `dependency_analyzer` was because of `const diff = "import a from 'a'"`.
117
+ // Let's use `stripStringsAndComments` but we DO NOT strip strings.
118
+ // We only strip comments.
119
+
120
+ // I'll define an inner function to just strip comments to be safe for imports.
121
+ // Let's stick to the simple `.replace` for comments for now, and rely on regex boundaries.
122
+ const semiCleanContent = content
123
+ .replace(/\/\*[\s\S]*?\*\//g, "")
124
+ .replace(/\/\/.*$/gm, "");
125
+
126
+ const importRegex2 =
127
+ /^[\s]*import(?:(?:[\w*\s{},]*)\sfrom\s+)?['"]([^'"]+)['"]/gm;
128
+ const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
129
+ const dynamicImportRegex = /import\(['"]([^'"]+)['"]\)/g;
130
+
131
+ const exportRegex =
132
+ /^[\s]*export\s+(?:const|let|var|function|class)\s+([a-zA-Z0-9_]+)/gm;
133
+ const moduleExportRegex = /module\.exports\s*=\s*\{([^}]+)\}/g;
134
+ const defaultExportRegex = /^[\s]*export\s+default\s+([a-zA-Z0-9_]+)/gm;
135
+
136
+ let match;
137
+ while ((match = importRegex2.exec(semiCleanContent)) !== null)
138
+ imports.add(match[1]);
139
+ while ((match = requireRegex.exec(semiCleanContent)) !== null)
140
+ imports.add(match[1]);
141
+ while ((match = dynamicImportRegex.exec(semiCleanContent)) !== null)
142
+ imports.add(match[1]);
143
+
144
+ while ((match = exportRegex.exec(semiCleanContent)) !== null)
145
+ exports.add(match[1]);
146
+ while ((match = defaultExportRegex.exec(semiCleanContent)) !== null)
147
+ exports.add(match[1]);
148
+
149
+ while ((match = moduleExportRegex.exec(semiCleanContent)) !== null) {
150
+ const tokens = match[1]
151
+ .split(",")
152
+ .map((s) => s.trim().split(":")[0].trim());
153
+ tokens.forEach((t) => t && exports.add(t));
154
+ }
155
+
156
+ return {
157
+ imports: Array.from(imports),
158
+ exports: Array.from(exports),
159
+ };
160
+ }
161
+
162
+ // ── YAML Generation ───────────────────────────────────────────────────────────
163
+ function generateYAML(data) {
164
+ let yaml = "# Auto-generated Architecture Graph by Tribunal Kit\n";
165
+ yaml += "# DO NOT EDIT MANUALLY - Auto-updates via incremental cache\n\n";
166
+
167
+ for (const [file, info] of Object.entries(data)) {
168
+ if (
169
+ info.imports.length === 0 &&
170
+ info.exports.length === 0 &&
171
+ (!info.dependents || info.dependents.length === 0)
172
+ )
173
+ continue;
174
+
175
+ yaml += `"${file}":\n`;
176
+ yaml += ` riskScore: "${info.riskScore || "Low"}"\n`;
177
+ yaml += ` blastRadius: ${info.blastRadius || 0}\n`;
178
+
179
+ if (info.imports && info.imports.length > 0) {
180
+ yaml += ` imports:\n`;
181
+ info.imports.forEach((i) => (yaml += ` - "${i}"\n`));
182
+ }
183
+ if (info.exports && info.exports.length > 0) {
184
+ yaml += ` exports:\n`;
185
+ info.exports.forEach((e) => (yaml += ` - "${e}"\n`));
186
+ }
187
+ if (info.dependents && info.dependents.length > 0) {
188
+ yaml += ` dependents:\n`;
189
+ info.dependents.forEach((d) => (yaml += ` - "${d}"\n`));
190
+ }
191
+ }
192
+ return yaml;
193
+ }
194
+
195
+ // ── Main Execution ────────────────────────────────────────────────────────────
196
+ function main() {
197
+ if (!fs.existsSync(AGENT_DIR)) {
198
+ console.error(`${RED}✖ Error: .agent directory not found.${RESET}`);
199
+ process.exit(1);
200
+ }
201
+
202
+ if (!fs.existsSync(HISTORY_DIR))
203
+ fs.mkdirSync(HISTORY_DIR, { recursive: true });
204
+
205
+ let cache = {};
206
+ if (fs.existsSync(CACHE_FILE)) {
207
+ try {
208
+ cache = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
209
+ } catch {
210
+ /* ignore */
211
+ }
212
+ }
213
+
214
+ const totalTimer = timer();
215
+ console.log(`${CYAN}✦ Building Architecture Graph...${RESET}`);
216
+ const files = walkDir(process.cwd());
217
+ const graphData = {};
218
+
219
+ let parsedCount = 0;
220
+ let cachedCount = 0;
221
+ const changedFiles = new Set();
222
+
223
+ for (const file of files) {
224
+ const relativePath = path.relative(process.cwd(), file).replace(/\\/g, "/");
225
+ let fileHash;
226
+ try {
227
+ fileHash = getFileHash(file);
228
+ } catch {
229
+ continue;
230
+ }
231
+
232
+ if (cache[relativePath] && cache[relativePath].hash === fileHash) {
233
+ graphData[relativePath] = {
234
+ imports: cache[relativePath].imports,
235
+ exports: cache[relativePath].exports,
236
+ };
237
+ cachedCount++;
238
+ } else {
239
+ try {
240
+ const content = fs.readFileSync(file, "utf8");
241
+ const parsed = parseFile(content);
242
+ graphData[relativePath] = parsed;
243
+
244
+ cache[relativePath] = {
245
+ hash: fileHash,
246
+ imports: parsed.imports,
247
+ exports: parsed.exports,
248
+ };
249
+ parsedCount++;
250
+ changedFiles.add(relativePath);
251
+ } catch {
252
+ /* ignore */
253
+ }
254
+ }
255
+ }
256
+
257
+ // Compute Dependents
258
+ for (const [_file, info] of Object.entries(graphData)) info.dependents = [];
259
+
260
+ const fileKeys = Object.keys(graphData);
261
+ for (const [file, info] of Object.entries(graphData)) {
262
+ for (const imp of info.imports) {
263
+ if (imp.startsWith(".")) {
264
+ let resolved = path.posix.join(path.dirname(file), imp);
265
+ // Look for direct match or .js / index.js
266
+ let matchingKey = fileKeys.find(
267
+ (k) =>
268
+ k === resolved ||
269
+ k === resolved + ".js" ||
270
+ k === resolved + ".ts" ||
271
+ k === resolved + "/index.js",
272
+ );
273
+ if (matchingKey) {
274
+ if (!graphData[matchingKey].dependents.includes(file)) {
275
+ graphData[matchingKey].dependents.push(file);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ // Compute Risk Score
283
+ function computeRisk(file) {
284
+ const visited = new Set();
285
+ function visit(node) {
286
+ if (visited.has(node)) return;
287
+ visited.add(node);
288
+ const deps = graphData[node]?.dependents || [];
289
+ deps.forEach(visit);
290
+ }
291
+ visit(file);
292
+ const radius = visited.size - 1;
293
+ let score = "Low";
294
+ if (radius > 10) score = "Critical";
295
+ else if (radius >= 5) score = "High";
296
+ else if (radius >= 2) score = "Medium";
297
+ return { score, count: Math.max(0, radius) };
298
+ }
299
+
300
+ for (const file of fileKeys) {
301
+ const risk = computeRisk(file);
302
+ graphData[file].riskScore = risk.score;
303
+ graphData[file].blastRadius = risk.count;
304
+
305
+ // Update cache with these values so visualizer can use it
306
+ if (cache[file]) {
307
+ cache[file].dependents = graphData[file].dependents;
308
+ cache[file].riskScore = risk.score;
309
+ cache[file].blastRadius = risk.count;
310
+ }
311
+ }
312
+
313
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
314
+ fs.writeFileSync(GRAPH_FILE, generateYAML(graphData));
315
+
316
+ // ── Pre-Computed Context Snapshots (Incremental) ─────────────────────────
317
+ const SNAPSHOTS_DIR = path.join(HISTORY_DIR, "snapshots");
318
+ if (!fs.existsSync(SNAPSHOTS_DIR)) {
319
+ fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true });
320
+ }
321
+
322
+ // Clean up snapshots for files that no longer exist
323
+ try {
324
+ const existingSnapshots = fs.readdirSync(SNAPSHOTS_DIR);
325
+ const currentFileSet = new Set(
326
+ fileKeys.map((f) => f.replace(/[\\/]/g, "__") + ".json"),
327
+ );
328
+ for (const snap of existingSnapshots) {
329
+ if (!currentFileSet.has(snap))
330
+ fs.unlinkSync(path.join(SNAPSHOTS_DIR, snap));
331
+ }
332
+ } catch {
333
+ /* ignore */
334
+ }
335
+
336
+ console.log(`${CYAN}✦ Generating Context Snapshots...${RESET}`);
337
+ let snapshotWritten = 0;
338
+ let snapshotSkipped = 0;
339
+ for (const file of fileKeys) {
340
+ const info = graphData[file];
341
+ const snapshotFile = file.replace(/[\\/]/g, "__") + ".json";
342
+ const snapshotPath = path.join(SNAPSHOTS_DIR, snapshotFile);
343
+
344
+ // Skip unchanged files that already have a snapshot
345
+ if (!changedFiles.has(file) && fs.existsSync(snapshotPath)) {
346
+ snapshotSkipped++;
347
+ continue;
348
+ }
349
+
350
+ let content = "";
351
+ try {
352
+ content = fs.readFileSync(path.join(process.cwd(), file), "utf8");
353
+ } catch {
354
+ continue;
355
+ }
356
+
357
+ const snapshot = {
358
+ file: file,
359
+ riskScore: info.riskScore,
360
+ blastRadius: info.blastRadius,
361
+ imports: {},
362
+ dependents: info.dependents || [],
363
+ content: content,
364
+ };
365
+
366
+ for (const imp of info.imports) {
367
+ if (imp.startsWith(".")) {
368
+ let resolved = path.posix.join(path.dirname(file), imp);
369
+ let matchingKey = fileKeys.find(
370
+ (k) =>
371
+ k === resolved ||
372
+ k === resolved + ".js" ||
373
+ k === resolved + ".ts" ||
374
+ k === resolved + "/index.js",
375
+ );
376
+ if (matchingKey && graphData[matchingKey]) {
377
+ snapshot.imports[imp] = graphData[matchingKey].exports;
378
+ } else {
379
+ snapshot.imports[imp] = [];
380
+ }
381
+ } else {
382
+ snapshot.imports[imp] = [];
383
+ }
384
+ }
385
+
386
+ fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
387
+ snapshotWritten++;
388
+ }
389
+ console.log(
390
+ ` ${DIM}Snapshots: ${snapshotWritten} written | ${snapshotSkipped} cached${RESET}`,
391
+ );
392
+
393
+ const totalMs = totalTimer();
394
+ console.log(`\n${GREEN}${BOLD}✔ Graph successfully built.${RESET}`);
395
+ console.log(
396
+ ` ${DIM}Parsed: ${parsedCount} files | Cached: ${cachedCount} files | ${formatMs(totalMs)}${RESET}`,
397
+ );
398
+ console.log(` ${DIM}Saved to: ${GRAPH_FILE}${RESET}`);
399
+ }
400
+ // ── Exports (for testing & programmatic use) ─────────────────────────────────
401
+ module.exports = {
402
+ parseFile,
403
+ generateYAML,
404
+ walkDir,
405
+ isExcluded,
406
+ getFileHash,
407
+ main,
408
+ };
409
+
410
+ if (require.main === module) {
411
+ main();
412
+ }