tribunal-kit 9.2.6 → 9.2.7

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 (63) hide show
  1. package/.agent/ARCHITECTURE.md +2 -2
  2. package/.agent/history/architecture-intelligence.html +1487 -401
  3. package/.agent/history/architecture-model.json +9365 -5697
  4. package/.agent/history/architecture-verification.json +37 -9
  5. package/.agent/routing_index.json +2069 -3340
  6. package/.agent/rules/GEMINI.md +1 -1
  7. package/.agent/scripts/adaptive_execution.js +511 -0
  8. package/.agent/scripts/architecture_diff.js +303 -260
  9. package/.agent/scripts/architecture_extractor.js +792 -753
  10. package/.agent/scripts/architecture_health.js +293 -256
  11. package/.agent/scripts/architecture_intelligence.js +3100 -0
  12. package/.agent/scripts/architecture_verifier.js +303 -270
  13. package/.agent/scripts/architecture_visualizer.js +880 -873
  14. package/.agent/scripts/blast_radius_engine.js +274 -234
  15. package/.agent/scripts/capability_evolution.js +453 -0
  16. package/.agent/scripts/concept_extractor.js +240 -0
  17. package/.agent/scripts/consideration_engine.js +284 -0
  18. package/.agent/scripts/coverage_engine.js +102 -0
  19. package/.agent/scripts/cross_domain_engine.js +1883 -0
  20. package/.agent/scripts/engineering_memory.js +1313 -0
  21. package/.agent/scripts/integrity_manifest.js +1 -5
  22. package/.agent/scripts/judgment_engine.js +790 -0
  23. package/.agent/scripts/materiality_engine.js +251 -0
  24. package/.agent/scripts/outcome_engine.js +624 -0
  25. package/.agent/scripts/outcome_optimization_engine.js +1632 -0
  26. package/.agent/scripts/simulation_engine.js +1478 -0
  27. package/.agent/scripts/skill_intelligence.js +1371 -0
  28. package/.agent/scripts/validation_engine.js +236 -236
  29. package/.agent/scripts/verification_auditor.js +266 -0
  30. package/.agent/skill-intelligence/taxonomy/engineering-concepts.json +555 -0
  31. package/.agent/skill_topic_map.json +301 -341
  32. package/.agent/skills_inventory.json +4272 -6922
  33. package/.agent/workflows/arch.md +96 -80
  34. package/.agent/workflows/skill-intelligence.md +216 -0
  35. package/CONTRIBUTING.md +2 -2
  36. package/README.md +24 -24
  37. package/bin/adapter-install.js +1 -1
  38. package/package.json +8 -9
  39. package/plugins/core-plugin.json +1 -1
  40. package/plugins/gemini/extension.json +1 -1
  41. package/src/browser/index.js +2 -1
  42. package/src/browser/launcher.js +41 -1
  43. package/src/cli.js +3 -1
  44. package/src/commands/arch.js +261 -172
  45. package/src/commands/context.js +27 -8
  46. package/src/commands/learn.js +21 -6
  47. package/src/commands/memory.js +10 -1
  48. package/src/commands/native.js +12 -3
  49. package/src/commands/skill-intelligence.js +1165 -0
  50. package/src/commands/system1.js +105 -4
  51. package/src/context/ranker.js +337 -339
  52. package/src/evolution/engine.js +296 -295
  53. package/src/synthesis/claim_check_separator.js +294 -271
  54. package/src/system1/browser_intelligence.js +300 -0
  55. package/src/system1/constants.js +49 -0
  56. package/src/system1/decision_engine.js +498 -0
  57. package/src/system1/evidence_engine.js +602 -0
  58. package/src/system1/evolution_pipeline.js +581 -0
  59. package/src/system1/index.d.ts +114 -0
  60. package/src/system1/index.js +102 -0
  61. package/src/system1/reviewer_orchestrator.js +477 -0
  62. package/src/tui/banner.js +1 -1
  63. package/src/utils/fs.js +27 -0
@@ -1,873 +1,880 @@
1
- #!/usr/bin/env node
2
- /**
3
- * architecture_visualizer.js — Tribunal Architecture Intelligence Explorer
4
- * ═════════════════════════════════════════════════════════════════════════════
5
- * Generates an ultra-premium, zero-dependency, self-contained standalone HTML
6
- * interactive architecture explorer with 5 dynamic projections:
7
- *
8
- * 1. System Topology (Services, Endpoints, Datastores, Queues)
9
- * 2. Security Trust Zones (Public Untrusted, DMZ, Authenticated, Datastore)
10
- * 3. Failure Architecture (Timeouts, Retries, Fallbacks, Circuit Breakers)
11
- * 4. Data Lineage & Persistence
12
- * 5. Interactive Blast Radius Simulator (Real-time downstream impact on click)
13
- *
14
- * Features:
15
- * - Offline zero-CDN execution
16
- * - Dark aesthetic with vibrant HSL design tokens
17
- * - Pan, zoom, search, node filtering, and focus isolation
18
- * - Live Evidence Drawer with verified source files, line numbers, and L1-L5 badges
19
- *
20
- * Zero external dependencies.
21
- */
22
-
23
- 'use strict';
24
-
25
- const fs = require('fs');
26
- const path = require('path');
27
- const { GREEN, CYAN, RED, DIM, RESET, BOX, banner, timer, formatMs } = require('./_colors');
28
-
29
- class ArchitectureVisualizer {
30
- constructor(model, verification = null) {
31
- this.model = model;
32
- this.verification = verification;
33
- }
34
-
35
- generateHtml() {
36
- const rawDataJson = JSON.stringify({
37
- model: this.model,
38
- verification: this.verification,
39
- }).replace(/</g, '\\u003c');
40
-
41
- return `<!DOCTYPE html>
42
- <html lang="en">
43
- <head>
44
- <meta charset="UTF-8">
45
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
46
- <title>Tribunal Architecture Intelligence Explorer</title>
47
- <style>
48
- :root {
49
- --bg: #09090b;
50
- --bg-panel: rgba(18, 18, 24, 0.85);
51
- --bg-card: #14141e;
52
- --border: #272738;
53
- --border-bright: #3f3f5a;
54
- --text: #f4f4f6;
55
- --text-dim: #9494a8;
56
- --accent-cyan: #06b6d4;
57
- --accent-blue: #3b82f6;
58
- --accent-green: #10b981;
59
- --accent-amber: #f59e0b;
60
- --accent-rose: #f43f5e;
61
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
62
- --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
63
- }
64
-
65
- * { box-sizing: border-box; margin: 0; padding: 0; }
66
- body {
67
- background: var(--bg);
68
- color: var(--text);
69
- font-family: var(--font-sans);
70
- height: 100vh;
71
- overflow: hidden;
72
- display: flex;
73
- flex-direction: column;
74
- }
75
-
76
- /* Top Navigation Bar */
77
- header {
78
- height: 56px;
79
- background: var(--bg-panel);
80
- backdrop-filter: blur(12px);
81
- border-bottom: 1px solid var(--border);
82
- display: flex;
83
- align-items: center;
84
- justify-content: space-between;
85
- padding: 0 20px;
86
- z-index: 50;
87
- flex-shrink: 0;
88
- }
89
-
90
- .brand {
91
- display: flex;
92
- align-items: center;
93
- gap: 10px;
94
- font-weight: 700;
95
- font-size: 15px;
96
- letter-spacing: -0.01em;
97
- }
98
- .brand-badge {
99
- background: linear-gradient(135deg, #06b6d4, #3b82f6);
100
- color: #000;
101
- font-size: 10px;
102
- font-weight: 800;
103
- padding: 2px 7px;
104
- border-radius: 4px;
105
- text-transform: uppercase;
106
- }
107
-
108
- /* Projection Tabs */
109
- .projection-tabs {
110
- display: flex;
111
- gap: 4px;
112
- background: rgba(0, 0, 0, 0.4);
113
- padding: 3px;
114
- border-radius: 8px;
115
- border: 1px solid var(--border);
116
- }
117
- .tab-btn {
118
- background: transparent;
119
- border: none;
120
- color: var(--text-dim);
121
- padding: 6px 14px;
122
- border-radius: 6px;
123
- font-size: 12px;
124
- font-weight: 600;
125
- cursor: pointer;
126
- transition: all 0.15s ease;
127
- }
128
- .tab-btn:hover { color: var(--text); }
129
- .tab-btn.active {
130
- background: var(--border-bright);
131
- color: #fff;
132
- box-shadow: 0 1px 3px rgba(0,0,0,0.3);
133
- }
134
-
135
- .header-actions {
136
- display: flex;
137
- align-items: center;
138
- gap: 12px;
139
- }
140
- .search-box {
141
- position: relative;
142
- }
143
- .search-box input {
144
- background: #14141e;
145
- border: 1px solid var(--border);
146
- border-radius: 6px;
147
- color: var(--text);
148
- font-size: 12px;
149
- padding: 6px 12px 6px 28px;
150
- outline: none;
151
- width: 220px;
152
- transition: border-color 0.15s ease;
153
- }
154
- .search-box input:focus { border-color: var(--accent-cyan); }
155
- .search-icon {
156
- position: absolute;
157
- left: 9px;
158
- top: 7px;
159
- color: var(--text-dim);
160
- font-size: 12px;
161
- }
162
-
163
- /* Main Canvas Area */
164
- .viewport {
165
- flex: 1;
166
- position: relative;
167
- overflow: hidden;
168
- cursor: grab;
169
- user-select: none;
170
- }
171
- .viewport:active { cursor: grabbing; }
172
-
173
- svg#arch-canvas {
174
- width: 100%;
175
- height: 100%;
176
- display: block;
177
- }
178
-
179
- /* Floating Controls */
180
- .canvas-controls {
181
- position: absolute;
182
- bottom: 20px;
183
- left: 20px;
184
- display: flex;
185
- gap: 6px;
186
- background: var(--bg-panel);
187
- backdrop-filter: blur(8px);
188
- padding: 4px;
189
- border-radius: 8px;
190
- border: 1px solid var(--border);
191
- z-index: 40;
192
- }
193
- .ctrl-btn {
194
- background: transparent;
195
- border: 1px solid transparent;
196
- color: var(--text);
197
- width: 32px;
198
- height: 32px;
199
- border-radius: 6px;
200
- display: flex;
201
- align-items: center;
202
- justify-content: center;
203
- cursor: pointer;
204
- font-size: 14px;
205
- transition: background 0.1s ease;
206
- }
207
- .ctrl-btn:hover { background: var(--border); }
208
-
209
- /* Legend Overlay */
210
- .legend-card {
211
- position: absolute;
212
- bottom: 20px;
213
- right: 20px;
214
- background: var(--bg-panel);
215
- backdrop-filter: blur(8px);
216
- padding: 12px 16px;
217
- border-radius: 8px;
218
- border: 1px solid var(--border);
219
- font-size: 11px;
220
- z-index: 40;
221
- max-width: 280px;
222
- }
223
- .legend-title {
224
- font-weight: 700;
225
- margin-bottom: 8px;
226
- text-transform: uppercase;
227
- letter-spacing: 0.05em;
228
- color: var(--text-dim);
229
- }
230
- .legend-item {
231
- display: flex;
232
- align-items: center;
233
- gap: 8px;
234
- margin-bottom: 4px;
235
- }
236
- .legend-dot {
237
- width: 10px;
238
- height: 10px;
239
- border-radius: 3px;
240
- }
241
-
242
- /* Slide-over Evidence Drawer */
243
- #evidence-drawer {
244
- position: absolute;
245
- top: 56px;
246
- right: 0;
247
- bottom: 0;
248
- width: 440px;
249
- background: var(--bg-panel);
250
- backdrop-filter: blur(16px);
251
- border-left: 1px solid var(--border);
252
- padding: 24px;
253
- overflow-y: auto;
254
- transform: translateX(100%);
255
- transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
256
- z-index: 60;
257
- }
258
- #evidence-drawer.open { transform: translateX(0); }
259
-
260
- .drawer-close {
261
- position: absolute;
262
- top: 16px;
263
- right: 16px;
264
- background: transparent;
265
- border: none;
266
- color: var(--text-dim);
267
- font-size: 18px;
268
- cursor: pointer;
269
- }
270
- .drawer-close:hover { color: var(--text); }
271
-
272
- .drawer-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
273
- .drawer-subtitle { font-size: 12px; color: var(--text-dim); margin-bottom: 16px; font-family: var(--font-mono); }
274
-
275
- .stat-pill-group {
276
- display: flex;
277
- gap: 8px;
278
- flex-wrap: wrap;
279
- margin-bottom: 20px;
280
- }
281
- .stat-pill {
282
- background: var(--bg-card);
283
- border: 1px solid var(--border);
284
- padding: 6px 12px;
285
- border-radius: 6px;
286
- font-size: 11px;
287
- }
288
- .stat-pill strong { color: var(--accent-cyan); }
289
-
290
- .section-title {
291
- font-size: 12px;
292
- font-weight: 700;
293
- text-transform: uppercase;
294
- letter-spacing: 0.05em;
295
- color: var(--text-dim);
296
- margin: 16px 0 8px 0;
297
- }
298
-
299
- .evidence-block {
300
- background: #0d0d14;
301
- border: 1px solid var(--border);
302
- border-radius: 6px;
303
- padding: 12px;
304
- margin-bottom: 8px;
305
- font-family: var(--font-mono);
306
- font-size: 11px;
307
- }
308
- .evidence-loc { color: var(--accent-cyan); font-weight: 600; margin-bottom: 4px; }
309
- .evidence-snippet {
310
- color: #cbd5e1;
311
- white-space: pre-wrap;
312
- word-break: break-all;
313
- background: rgba(0,0,0,0.3);
314
- padding: 6px;
315
- border-radius: 4px;
316
- margin-top: 6px;
317
- }
318
- .confidence-badge {
319
- display: inline-block;
320
- padding: 2px 6px;
321
- border-radius: 4px;
322
- font-size: 9px;
323
- font-weight: 800;
324
- text-transform: uppercase;
325
- }
326
- .conf-L1 { background: #065f46; color: #34d399; }
327
- .conf-L2 { background: #1e3a8a; color: #60a5fa; }
328
- .conf-L3 { background: #78350f; color: #fbbf24; }
329
- .conf-L5 { background: #881337; color: #f43f5e; }
330
-
331
- /* SVG Graphical Node Styling */
332
- .node-group { cursor: pointer; transition: opacity 0.2s; }
333
- .node-box {
334
- stroke-width: 1.5px;
335
- rx: 8px;
336
- transition: all 0.2s ease;
337
- }
338
- .node-group:hover .node-box {
339
- stroke-width: 2.5px;
340
- filter: drop-shadow(0 0 8px rgba(6, 182, 212, 0.4));
341
- }
342
- .node-text-title {
343
- font-family: var(--font-sans);
344
- font-weight: 700;
345
- fill: #ffffff;
346
- font-size: 12px;
347
- pointer-events: none;
348
- }
349
- .node-text-sub {
350
- font-family: var(--font-sans);
351
- font-size: 10px;
352
- fill: #9494a8;
353
- pointer-events: none;
354
- }
355
- .edge-path {
356
- fill: none;
357
- stroke-width: 1.5px;
358
- transition: stroke 0.2s, stroke-width 0.2s;
359
- }
360
- .edge-marker { fill: #475569; }
361
-
362
- /* Dimming & Highlight states */
363
- .dimmed { opacity: 0.15; }
364
- .highlighted { opacity: 1 !important; }
365
- .highlighted-edge { stroke: #06b6d4 !important; stroke-width: 2.5px !important; }
366
- .blast-ring-1 { stroke: #f43f5e !important; stroke-width: 3px !important; }
367
- .blast-ring-2 { stroke: #f59e0b !important; stroke-width: 2.5px !important; }
368
- .blast-ring-3 { stroke: #3b82f6 !important; stroke-width: 2px !important; }
369
- </style>
370
- </head>
371
- <body>
372
-
373
- <header>
374
- <div class="brand">
375
- <span>⚖️ Tribunal Architecture Intelligence</span>
376
- <span class="brand-badge">TAI Fact Graph</span>
377
- </div>
378
-
379
- <div class="projection-tabs">
380
- <button class="tab-btn active" data-projection="topology">Topology</button>
381
- <button class="tab-btn" data-projection="security">Security Zones</button>
382
- <button class="tab-btn" data-projection="failure">Failure Modes</button>
383
- <button class="tab-btn" data-projection="blast">Blast Radius Simulator</button>
384
- </div>
385
-
386
- <div class="header-actions">
387
- <div class="search-box">
388
- <span class="search-icon">🔍</span>
389
- <input type="text" id="node-search" placeholder="Search architecture...">
390
- </div>
391
- </div>
392
- </header>
393
-
394
- <main class="viewport" id="viewport">
395
- <svg id="arch-canvas">
396
- <defs>
397
- <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
398
- <path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#64748b" />
399
- </marker>
400
- <marker id="arrow-active" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
401
- <path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#06b6d4" />
402
- </marker>
403
- <marker id="arrow-warn" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
404
- <path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#f59e0b" />
405
- </marker>
406
- </defs>
407
- <g id="world-layer">
408
- <g id="zones-layer"></g>
409
- <g id="edges-layer"></g>
410
- <g id="nodes-layer"></g>
411
- </g>
412
- </svg>
413
-
414
- <div class="canvas-controls">
415
- <button class="ctrl-btn" id="btn-zoom-in" title="Zoom In">+</button>
416
- <button class="ctrl-btn" id="btn-zoom-out" title="Zoom Out">−</button>
417
- <button class="ctrl-btn" id="btn-zoom-reset" title="Fit Screen">⛶</button>
418
- </div>
419
-
420
- <div class="legend-card" id="legend">
421
- <div class="legend-title" id="legend-title">System Topology</div>
422
- <div id="legend-items"></div>
423
- </div>
424
- </main>
425
-
426
- <aside id="evidence-drawer">
427
- <button class="drawer-close" id="drawer-close">✕</button>
428
- <div id="drawer-content"></div>
429
- </aside>
430
-
431
- <script id="tribunal-architecture-data" type="application/json">
432
- ${rawDataJson}
433
- </script>
434
-
435
- <script>
436
- (function () {
437
- const rawData = JSON.parse(document.getElementById('tribunal-architecture-data').textContent);
438
- const model = rawData.model;
439
- const entities = model.entities || [];
440
- const relationships = model.relationships || [];
441
- const trustBoundaries = model.trustBoundaries || [];
442
-
443
- let currentProjection = 'topology';
444
- let selectedNodeId = null;
445
-
446
- // Transform & Pan/Zoom State
447
- let transform = { x: 80, y: 80, k: 0.75 };
448
- let isPanning = false;
449
- let startPoint = { x: 0, y: 0 };
450
-
451
- const viewport = document.getElementById('viewport');
452
- const world = document.getElementById('world-layer');
453
- const nodesLayer = document.getElementById('nodes-layer');
454
- const edgesLayer = document.getElementById('edges-layer');
455
- const zonesLayer = document.getElementById('zones-layer');
456
- const drawer = document.getElementById('evidence-drawer');
457
- const drawerContent = document.getElementById('drawer-content');
458
- const drawerClose = document.getElementById('drawer-close');
459
-
460
- // ── Build Spatial Layout with Semantic Column Zoning ──────────────────
461
- const NODE_W = 160;
462
- const NODE_H = 60;
463
- const GAP_X = 240;
464
- const GAP_Y = 90;
465
-
466
- const columnMap = {
467
- endpoint: 0,
468
- module: 1,
469
- event_bus: 2,
470
- queue: 2,
471
- cache: 2,
472
- datastore: 3,
473
- external_api: 3,
474
- identity_provider: 0,
475
- test_suite: 1,
476
- };
477
-
478
- const columns = [[], [], [], []];
479
- entities.forEach(e => {
480
- const col = columnMap[e.kind] ?? 1;
481
- columns[col].push(e);
482
- });
483
-
484
- const nodeCoords = new Map();
485
- columns.forEach((colEntities, colIdx) => {
486
- colEntities.forEach((entity, rowIdx) => {
487
- nodeCoords.set(entity.id, {
488
- x: colIdx * (NODE_W + GAP_X) + 100,
489
- y: rowIdx * (NODE_H + GAP_Y) + 100,
490
- w: NODE_W,
491
- h: NODE_H,
492
- entity
493
- });
494
- });
495
- });
496
-
497
- // ── Render Graph ──────────────────────────────────────────────────────
498
- function updateCanvasTransform() {
499
- world.setAttribute('transform', \`translate(\${transform.x}, \${transform.y}) scale(\${transform.k})\`);
500
- }
501
-
502
- function renderEdges() {
503
- edgesLayer.innerHTML = '';
504
- relationships.forEach(rel => {
505
- const src = nodeCoords.get(rel.sourceId);
506
- const tgt = nodeCoords.get(rel.targetId);
507
- if (!src || !tgt) return;
508
-
509
- const x1 = src.x + src.w;
510
- const y1 = src.y + src.h / 2;
511
- const x2 = tgt.x;
512
- const y2 = tgt.y + tgt.h / 2;
513
-
514
- const dx = (x2 - x1) / 2;
515
- const d = \`M \${x1} \${y1} C \${x1 + dx} \${y1}, \${x2 - dx} \${y2}, \${x2} \${y2}\`;
516
-
517
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
518
- path.setAttribute('d', d);
519
- path.setAttribute('class', 'edge-path');
520
- path.setAttribute('data-id', rel.id);
521
- path.setAttribute('data-source', rel.sourceId);
522
- path.setAttribute('data-target', rel.targetId);
523
-
524
- let strokeColor = '#334155';
525
- let marker = 'url(#arrow)';
526
-
527
- if (currentProjection === 'failure' && rel.failureMode && !rel.failureMode.hasTimeout) {
528
- strokeColor = '#f59e0b';
529
- marker = 'url(#arrow-warn)';
530
- }
531
-
532
- path.setAttribute('stroke', strokeColor);
533
- path.setAttribute('marker-end', marker);
534
- edgesLayer.appendChild(path);
535
- });
536
- }
537
-
538
- function renderNodes() {
539
- nodesLayer.innerHTML = '';
540
- entities.forEach(entity => {
541
- const coord = nodeCoords.get(entity.id);
542
- if (!coord) return;
543
-
544
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
545
- g.setAttribute('class', 'node-group');
546
- g.setAttribute('data-id', entity.id);
547
- g.setAttribute('transform', \`translate(\${coord.x}, \${coord.y})\`);
548
-
549
- // Color tokens by projection
550
- let stroke = '#272738';
551
- let fill = '#14141e';
552
-
553
- if (currentProjection === 'topology') {
554
- if (entity.kind === 'endpoint') stroke = '#06b6d4';
555
- else if (entity.kind === 'datastore' || entity.kind === 'cache') stroke = '#3b82f6';
556
- else if (entity.kind === 'queue' || entity.kind === 'event_bus') stroke = '#a855f7';
557
- else if (entity.kind === 'external_api') stroke = '#f43f5e';
558
- else stroke = '#475569';
559
- } else if (currentProjection === 'security') {
560
- if (entity.trustZone === 'public_untrusted') stroke = '#f43f5e';
561
- else if (entity.trustZone === 'dmz_gateway') stroke = '#f59e0b';
562
- else if (entity.trustZone === 'internal_service') stroke = '#10b981';
563
- else if (entity.trustZone === 'isolated_datastore') stroke = '#3b82f6';
564
- else stroke = '#64748b';
565
- } else if (currentProjection === 'failure') {
566
- if (entity.kind === 'external_api') stroke = '#f59e0b';
567
- else stroke = '#334155';
568
- }
569
-
570
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
571
- rect.setAttribute('width', coord.w);
572
- rect.setAttribute('height', coord.h);
573
- rect.setAttribute('class', 'node-box');
574
- rect.setAttribute('fill', fill);
575
- rect.setAttribute('stroke', stroke);
576
-
577
- const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
578
- title.setAttribute('x', 12);
579
- title.setAttribute('y', 26);
580
- title.setAttribute('class', 'node-text-title');
581
- const cleanName = entity.name.length > 18 ? entity.name.slice(0, 16) + '...' : entity.name;
582
- title.textContent = cleanName;
583
-
584
- const sub = document.createElementNS('http://www.w3.org/2000/svg', 'text');
585
- sub.setAttribute('x', 12);
586
- sub.setAttribute('y', 44);
587
- sub.setAttribute('class', 'node-text-sub');
588
- sub.textContent = entity.kind.replace('_', ' ').toUpperCase();
589
-
590
- g.appendChild(rect);
591
- g.appendChild(title);
592
- g.appendChild(sub);
593
-
594
- g.addEventListener('click', (ev) => {
595
- ev.stopPropagation();
596
- onNodeClick(entity.id);
597
- });
598
-
599
- nodesLayer.appendChild(g);
600
- });
601
- }
602
-
603
- function updateLegend() {
604
- const titleEl = document.getElementById('legend-title');
605
- const itemsEl = document.getElementById('legend-items');
606
-
607
- const legends = {
608
- topology: {
609
- title: 'System Topology',
610
- items: [
611
- { color: '#06b6d4', label: 'HTTP API Endpoint' },
612
- { color: '#475569', label: 'Internal Module' },
613
- { color: '#3b82f6', label: 'Datastore / Cache' },
614
- { color: '#a855f7', label: 'Queue / Event Channel' },
615
- { color: '#f43f5e', label: 'External Service' },
616
- ]
617
- },
618
- security: {
619
- title: 'Security Trust Zones',
620
- items: [
621
- { color: '#f43f5e', label: 'Public Untrusted' },
622
- { color: '#f59e0b', label: 'DMZ Gateway / Auth' },
623
- { color: '#10b981', label: 'Authenticated Service' },
624
- { color: '#3b82f6', label: 'Isolated Datastore' },
625
- ]
626
- },
627
- failure: {
628
- title: 'Failure Architecture',
629
- items: [
630
- { color: '#f59e0b', label: 'External (No Timeout)' },
631
- { color: '#10b981', label: 'Resilient RPC' },
632
- ]
633
- },
634
- blast: {
635
- title: 'Blast Radius Simulator',
636
- items: [
637
- { color: '#f43f5e', label: 'Ring 1 (Direct Impact)' },
638
- { color: '#f59e0b', label: 'Ring 2 (Transitive Depth 2)' },
639
- { color: '#3b82f6', label: 'Ring 3+ (Deep Transitive)' },
640
- ]
641
- }
642
- };
643
-
644
- const cur = legends[currentProjection] || legends.topology;
645
- titleEl.textContent = cur.title;
646
- itemsEl.innerHTML = cur.items.map(it => \`
647
- <div class="legend-item">
648
- <div class="legend-dot" style="background: \${it.color}"></div>
649
- <span>\${it.label}</span>
650
- </div>
651
- \`).join('');
652
- }
653
-
654
- // ── Interactive Selection & Blast Simulation ─────────────────────────
655
- function onNodeClick(id) {
656
- selectedNodeId = id;
657
- const entity = entities.find(e => e.id === id);
658
- if (!entity) return;
659
-
660
- // Reset styling
661
- document.querySelectorAll('.node-group').forEach(el => el.classList.remove('dimmed', 'highlighted'));
662
- document.querySelectorAll('.edge-path').forEach(el => {
663
- el.classList.remove('dimmed', 'highlighted-edge', 'blast-ring-1', 'blast-ring-2', 'blast-ring-3');
664
- });
665
-
666
- if (currentProjection === 'blast') {
667
- // Live Blast Simulation
668
- const ring1 = new Set();
669
- const ring2 = new Set();
670
- const ring3 = new Set();
671
-
672
- relationships.forEach(r => {
673
- if (r.targetId === id) ring1.add(r.sourceId);
674
- });
675
- relationships.forEach(r => {
676
- if (ring1.has(r.targetId) && r.sourceId !== id) ring2.add(r.sourceId);
677
- });
678
- relationships.forEach(r => {
679
- if (ring2.has(r.targetId) && !ring1.has(r.sourceId) && r.sourceId !== id) ring3.add(r.sourceId);
680
- });
681
-
682
- document.querySelectorAll('.node-group').forEach(el => {
683
- const elId = el.getAttribute('data-id');
684
- if (elId === id) el.classList.add('highlighted');
685
- else if (ring1.has(elId) || ring2.has(elId) || ring3.has(elId)) el.classList.add('highlighted');
686
- else el.classList.add('dimmed');
687
- });
688
-
689
- document.querySelectorAll('.edge-path').forEach(el => {
690
- const tgt = el.getAttribute('data-target');
691
- if (tgt === id) el.classList.add('blast-ring-1');
692
- else if (ring1.has(tgt)) el.classList.add('blast-ring-2');
693
- else if (ring2.has(tgt)) el.classList.add('blast-ring-3');
694
- else el.classList.add('dimmed');
695
- });
696
- } else {
697
- // Standard connected focus
698
- const connectedNodes = new Set([id]);
699
- relationships.forEach(r => {
700
- if (r.sourceId === id) connectedNodes.add(r.targetId);
701
- if (r.targetId === id) connectedNodes.add(r.sourceId);
702
- });
703
-
704
- document.querySelectorAll('.node-group').forEach(el => {
705
- const elId = el.getAttribute('data-id');
706
- if (connectedNodes.has(elId)) el.classList.add('highlighted');
707
- else el.classList.add('dimmed');
708
- });
709
-
710
- document.querySelectorAll('.edge-path').forEach(el => {
711
- const s = el.getAttribute('data-source');
712
- const t = el.getAttribute('data-target');
713
- if (s === id || t === id) el.classList.add('highlighted-edge');
714
- else el.classList.add('dimmed');
715
- });
716
- }
717
-
718
- openDrawer(entity);
719
- }
720
-
721
- function openDrawer(entity) {
722
- drawer.classList.add('open');
723
- const sources = entity.sources || [];
724
-
725
- drawerContent.innerHTML = \`
726
- <h2 class="drawer-title">\${entity.name}</h2>
727
- <div class="drawer-subtitle">\${entity.id}</div>
728
-
729
- <div class="stat-pill-group">
730
- <div class="stat-pill">Kind: <strong>\${entity.kind}</strong></div>
731
- <div class="stat-pill">Zone: <strong>\${entity.trustZone || 'none'}</strong></div>
732
- <div class="stat-pill">Risk: <strong>\${entity.blastRadius?.riskScore || 0.1}</strong></div>
733
- <div class="stat-pill">Confidence: <span class="confidence-badge conf-\${entity.confidence || 'L1'}">\${entity.confidence || 'L1'} FACT</span></div>
734
- </div>
735
-
736
- <div class="section-title">Verified Source Code Evidence</div>
737
- \${sources.map(s => \`
738
- <div class="evidence-block">
739
- <div class="evidence-loc">\${s.file}:\${s.startLine}\${s.endLine ? '-' + s.endLine : ''}</div>
740
- <div>Hash: <code>\${s.contentHash || 'verified'}</code></div>
741
- \${s.snippet ? \`<div class="evidence-snippet">\${s.snippet}</div>\` : ''}
742
- </div>
743
- \`).join('')}
744
-
745
- <div class="section-title">Architectural Role & Description</div>
746
- <p style="font-size: 13px; line-height: 1.5; color: #cbd5e1; margin-bottom: 16px;">
747
- \${entity.role || 'Internal architectural component extracted deterministically from codebase AST.'}
748
- </p>
749
- \`;
750
- }
751
-
752
- drawerClose.addEventListener('click', () => {
753
- drawer.classList.remove('open');
754
- document.querySelectorAll('.node-group').forEach(el => el.classList.remove('dimmed', 'highlighted'));
755
- document.querySelectorAll('.edge-path').forEach(el => {
756
- el.classList.remove('dimmed', 'highlighted-edge', 'blast-ring-1', 'blast-ring-2', 'blast-ring-3');
757
- });
758
- });
759
-
760
- // ── Event Handlers: Pan / Zoom / Tabs ────────────────────────────────
761
- viewport.addEventListener('mousedown', (e) => {
762
- if (e.target.closest('.node-group')) return;
763
- isPanning = true;
764
- startPoint = { x: e.clientX - transform.x, y: e.clientY - transform.y };
765
- });
766
- window.addEventListener('mousemove', (e) => {
767
- if (!isPanning) return;
768
- transform.x = e.clientX - startPoint.x;
769
- transform.y = e.clientY - startPoint.y;
770
- updateCanvasTransform();
771
- });
772
- window.addEventListener('mouseup', () => { isPanning = false; });
773
-
774
- viewport.addEventListener('wheel', (e) => {
775
- e.preventDefault();
776
- const factor = e.deltaY > 0 ? 0.9 : 1.1;
777
- transform.k = Math.max(0.2, Math.min(3.0, transform.k * factor));
778
- updateCanvasTransform();
779
- });
780
-
781
- document.getElementById('btn-zoom-in').addEventListener('click', () => {
782
- transform.k = Math.min(3.0, transform.k * 1.2);
783
- updateCanvasTransform();
784
- });
785
- document.getElementById('btn-zoom-out').addEventListener('click', () => {
786
- transform.k = Math.max(0.2, transform.k / 1.2);
787
- updateCanvasTransform();
788
- });
789
- document.getElementById('btn-zoom-reset').addEventListener('click', () => {
790
- transform = { x: 80, y: 80, k: 0.75 };
791
- updateCanvasTransform();
792
- });
793
-
794
- document.querySelectorAll('.tab-btn').forEach(btn => {
795
- btn.addEventListener('click', () => {
796
- document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
797
- btn.classList.add('active');
798
- currentProjection = btn.getAttribute('data-projection');
799
- updateLegend();
800
- renderNodes();
801
- renderEdges();
802
- if (selectedNodeId) onNodeClick(selectedNodeId);
803
- });
804
- });
805
-
806
- // Search Handler
807
- document.getElementById('node-search').addEventListener('input', (e) => {
808
- const query = e.target.value.toLowerCase().trim();
809
- if (!query) {
810
- document.querySelectorAll('.node-group').forEach(el => el.classList.remove('dimmed', 'highlighted'));
811
- return;
812
- }
813
- document.querySelectorAll('.node-group').forEach(el => {
814
- const id = el.getAttribute('data-id').toLowerCase();
815
- const ent = entities.find(x => x.id.toLowerCase() === id);
816
- if (id.includes(query) || (ent && ent.name.toLowerCase().includes(query))) {
817
- el.classList.add('highlighted');
818
- el.classList.remove('dimmed');
819
- } else {
820
- el.classList.add('dimmed');
821
- el.classList.remove('highlighted');
822
- }
823
- });
824
- });
825
-
826
- // Initial Mount
827
- updateCanvasTransform();
828
- updateLegend();
829
- renderNodes();
830
- renderEdges();
831
- })();
832
- </script>
833
- </body>
834
- </html>`;
835
- }
836
-
837
- saveHtml(outputPath) {
838
- const dest = outputPath || path.join(process.cwd(), '.agent', 'history', 'architecture-intelligence.html');
839
- fs.mkdirSync(path.dirname(dest), { recursive: true });
840
- const html = this.generateHtml();
841
- fs.writeFileSync(dest, html, 'utf8');
842
- return dest;
843
- }
844
- }
845
-
846
- // ── Standalone CLI ────────────────────────────────────────────────────────────
847
- if (require.main === module) {
848
- const t = timer();
849
- const repoRoot = process.cwd();
850
- const modelFile = path.join(repoRoot, '.agent', 'history', 'architecture-model.json');
851
- const verifFile = path.join(repoRoot, '.agent', 'history', 'architecture-verification.json');
852
-
853
- console.log(banner('Tribunal Architecture Intelligence Visualizer'));
854
-
855
- if (!fs.existsSync(modelFile)) {
856
- console.error(` ${RED}✖ architecture-model.json not found.${RESET} Run architecture_extractor.js first.`);
857
- process.exit(1);
858
- }
859
-
860
- const model = JSON.parse(fs.readFileSync(modelFile, 'utf8'));
861
- const verification = fs.existsSync(verifFile) ? JSON.parse(fs.readFileSync(verifFile, 'utf8')) : null;
862
-
863
- const visualizer = new ArchitectureVisualizer(model, verification);
864
- const outPath = visualizer.saveHtml();
865
-
866
- console.log(` ${GREEN}${BOX.check} Interactive Architecture Explorer Generated!${RESET}`);
867
- console.log(` ${DIM}File:${RESET} ${CYAN}${outPath}${RESET} (${formatMs(t())})`);
868
- console.log(` ${DIM}Projections Enabled:${RESET} Topology, Security Zones, Failure Modes, Blast Simulator\n`);
869
- }
870
-
871
- module.exports = {
872
- ArchitectureVisualizer,
873
- };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * architecture_visualizer.js — Tribunal Architecture Intelligence Explorer
4
+ * ═════════════════════════════════════════════════════════════════════════════
5
+ * Generates an ultra-premium, zero-dependency, self-contained standalone HTML
6
+ * interactive architecture explorer with 5 dynamic projections:
7
+ *
8
+ * 1. System Topology (Services, Endpoints, Datastores, Queues)
9
+ * 2. Security Trust Zones (Public Untrusted, DMZ, Authenticated, Datastore)
10
+ * 3. Failure Architecture (Timeouts, Retries, Fallbacks, Circuit Breakers)
11
+ * 4. Data Lineage & Persistence
12
+ * 5. Interactive Blast Radius Simulator (Real-time downstream impact on click)
13
+ *
14
+ * Features:
15
+ * - Offline zero-CDN execution
16
+ * - Dark aesthetic with vibrant HSL design tokens
17
+ * - Pan, zoom, search, node filtering, and focus isolation
18
+ * - Live Evidence Drawer with verified source files, line numbers, and L1-L5 badges
19
+ *
20
+ * Zero external dependencies.
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { GREEN, CYAN, RED, DIM, RESET, BOX, banner, timer, formatMs } = require('./_colors');
28
+
29
+ class ArchitectureVisualizer {
30
+ constructor(model, verification = null) {
31
+ this.model = model;
32
+ this.verification = verification;
33
+ }
34
+
35
+ generateHtml() {
36
+ const rawDataJson = JSON.stringify({
37
+ model: this.model,
38
+ verification: this.verification,
39
+ }).replace(/</g, '\\u003c');
40
+
41
+ return `<!DOCTYPE html>
42
+ <html lang="en">
43
+ <head>
44
+ <meta charset="UTF-8">
45
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
46
+ <title>Tribunal Architecture Intelligence Explorer</title>
47
+ <style>
48
+ :root {
49
+ --bg: #09090b;
50
+ --bg-panel: rgba(18, 18, 24, 0.85);
51
+ --bg-card: #14141e;
52
+ --border: #272738;
53
+ --border-bright: #3f3f5a;
54
+ --text: #f4f4f6;
55
+ --text-dim: #9494a8;
56
+ --accent-cyan: #06b6d4;
57
+ --accent-blue: #3b82f6;
58
+ --accent-green: #10b981;
59
+ --accent-amber: #f59e0b;
60
+ --accent-rose: #f43f5e;
61
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
62
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
63
+ }
64
+
65
+ * { box-sizing: border-box; margin: 0; padding: 0; }
66
+ body {
67
+ background: var(--bg);
68
+ color: var(--text);
69
+ font-family: var(--font-sans);
70
+ height: 100vh;
71
+ overflow: hidden;
72
+ display: flex;
73
+ flex-direction: column;
74
+ }
75
+
76
+ /* Top Navigation Bar */
77
+ header {
78
+ height: 56px;
79
+ background: var(--bg-panel);
80
+ backdrop-filter: blur(12px);
81
+ border-bottom: 1px solid var(--border);
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: space-between;
85
+ padding: 0 20px;
86
+ z-index: 50;
87
+ flex-shrink: 0;
88
+ }
89
+
90
+ .brand {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 10px;
94
+ font-weight: 700;
95
+ font-size: 15px;
96
+ letter-spacing: -0.01em;
97
+ }
98
+ .brand-badge {
99
+ background: linear-gradient(135deg, #06b6d4, #3b82f6);
100
+ color: #000;
101
+ font-size: 10px;
102
+ font-weight: 800;
103
+ padding: 2px 7px;
104
+ border-radius: 4px;
105
+ text-transform: uppercase;
106
+ }
107
+
108
+ /* Projection Tabs */
109
+ .projection-tabs {
110
+ display: flex;
111
+ gap: 4px;
112
+ background: rgba(0, 0, 0, 0.4);
113
+ padding: 3px;
114
+ border-radius: 8px;
115
+ border: 1px solid var(--border);
116
+ }
117
+ .tab-btn {
118
+ background: transparent;
119
+ border: none;
120
+ color: var(--text-dim);
121
+ padding: 6px 14px;
122
+ border-radius: 6px;
123
+ font-size: 12px;
124
+ font-weight: 600;
125
+ cursor: pointer;
126
+ transition: all 0.15s ease;
127
+ }
128
+ .tab-btn:hover { color: var(--text); }
129
+ .tab-btn.active {
130
+ background: var(--border-bright);
131
+ color: #fff;
132
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
133
+ }
134
+
135
+ .header-actions {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 12px;
139
+ }
140
+ .search-box {
141
+ position: relative;
142
+ }
143
+ .search-box input {
144
+ background: #14141e;
145
+ border: 1px solid var(--border);
146
+ border-radius: 6px;
147
+ color: var(--text);
148
+ font-size: 12px;
149
+ padding: 6px 12px 6px 28px;
150
+ outline: none;
151
+ width: 220px;
152
+ transition: border-color 0.15s ease;
153
+ }
154
+ .search-box input:focus { border-color: var(--accent-cyan); }
155
+ .search-icon {
156
+ position: absolute;
157
+ left: 9px;
158
+ top: 7px;
159
+ color: var(--text-dim);
160
+ font-size: 12px;
161
+ }
162
+
163
+ /* Main Canvas Area */
164
+ .viewport {
165
+ flex: 1;
166
+ position: relative;
167
+ overflow: hidden;
168
+ cursor: grab;
169
+ user-select: none;
170
+ }
171
+ .viewport:active { cursor: grabbing; }
172
+
173
+ svg#arch-canvas {
174
+ width: 100%;
175
+ height: 100%;
176
+ display: block;
177
+ }
178
+
179
+ /* Floating Controls */
180
+ .canvas-controls {
181
+ position: absolute;
182
+ bottom: 20px;
183
+ left: 20px;
184
+ display: flex;
185
+ gap: 6px;
186
+ background: var(--bg-panel);
187
+ backdrop-filter: blur(8px);
188
+ padding: 4px;
189
+ border-radius: 8px;
190
+ border: 1px solid var(--border);
191
+ z-index: 40;
192
+ }
193
+ .ctrl-btn {
194
+ background: transparent;
195
+ border: 1px solid transparent;
196
+ color: var(--text);
197
+ width: 32px;
198
+ height: 32px;
199
+ border-radius: 6px;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ cursor: pointer;
204
+ font-size: 14px;
205
+ transition: background 0.1s ease;
206
+ }
207
+ .ctrl-btn:hover { background: var(--border); }
208
+
209
+ /* Legend Overlay */
210
+ .legend-card {
211
+ position: absolute;
212
+ bottom: 20px;
213
+ right: 20px;
214
+ background: var(--bg-panel);
215
+ backdrop-filter: blur(8px);
216
+ padding: 12px 16px;
217
+ border-radius: 8px;
218
+ border: 1px solid var(--border);
219
+ font-size: 11px;
220
+ z-index: 40;
221
+ max-width: 280px;
222
+ }
223
+ .legend-title {
224
+ font-weight: 700;
225
+ margin-bottom: 8px;
226
+ text-transform: uppercase;
227
+ letter-spacing: 0.05em;
228
+ color: var(--text-dim);
229
+ }
230
+ .legend-item {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 8px;
234
+ margin-bottom: 4px;
235
+ }
236
+ .legend-dot {
237
+ width: 10px;
238
+ height: 10px;
239
+ border-radius: 3px;
240
+ }
241
+
242
+ /* Slide-over Evidence Drawer */
243
+ #evidence-drawer {
244
+ position: absolute;
245
+ top: 56px;
246
+ right: 0;
247
+ bottom: 0;
248
+ width: 440px;
249
+ background: var(--bg-panel);
250
+ backdrop-filter: blur(16px);
251
+ border-left: 1px solid var(--border);
252
+ padding: 24px;
253
+ overflow-y: auto;
254
+ transform: translateX(100%);
255
+ transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
256
+ z-index: 60;
257
+ }
258
+ #evidence-drawer.open { transform: translateX(0); }
259
+
260
+ .drawer-close {
261
+ position: absolute;
262
+ top: 16px;
263
+ right: 16px;
264
+ background: transparent;
265
+ border: none;
266
+ color: var(--text-dim);
267
+ font-size: 18px;
268
+ cursor: pointer;
269
+ }
270
+ .drawer-close:hover { color: var(--text); }
271
+
272
+ .drawer-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
273
+ .drawer-subtitle { font-size: 12px; color: var(--text-dim); margin-bottom: 16px; font-family: var(--font-mono); }
274
+
275
+ .stat-pill-group {
276
+ display: flex;
277
+ gap: 8px;
278
+ flex-wrap: wrap;
279
+ margin-bottom: 20px;
280
+ }
281
+ .stat-pill {
282
+ background: var(--bg-card);
283
+ border: 1px solid var(--border);
284
+ padding: 6px 12px;
285
+ border-radius: 6px;
286
+ font-size: 11px;
287
+ }
288
+ .stat-pill strong { color: var(--accent-cyan); }
289
+
290
+ .section-title {
291
+ font-size: 12px;
292
+ font-weight: 700;
293
+ text-transform: uppercase;
294
+ letter-spacing: 0.05em;
295
+ color: var(--text-dim);
296
+ margin: 16px 0 8px 0;
297
+ }
298
+
299
+ .evidence-block {
300
+ background: #0d0d14;
301
+ border: 1px solid var(--border);
302
+ border-radius: 6px;
303
+ padding: 12px;
304
+ margin-bottom: 8px;
305
+ font-family: var(--font-mono);
306
+ font-size: 11px;
307
+ }
308
+ .evidence-loc { color: var(--accent-cyan); font-weight: 600; margin-bottom: 4px; }
309
+ .evidence-snippet {
310
+ color: #cbd5e1;
311
+ white-space: pre-wrap;
312
+ word-break: break-all;
313
+ background: rgba(0,0,0,0.3);
314
+ padding: 6px;
315
+ border-radius: 4px;
316
+ margin-top: 6px;
317
+ }
318
+ .confidence-badge {
319
+ display: inline-block;
320
+ padding: 2px 6px;
321
+ border-radius: 4px;
322
+ font-size: 9px;
323
+ font-weight: 800;
324
+ text-transform: uppercase;
325
+ }
326
+ .conf-L1 { background: #065f46; color: #34d399; }
327
+ .conf-L2 { background: #1e3a8a; color: #60a5fa; }
328
+ .conf-L3 { background: #78350f; color: #fbbf24; }
329
+ .conf-L5 { background: #881337; color: #f43f5e; }
330
+
331
+ /* SVG Graphical Node Styling */
332
+ .node-group { cursor: pointer; transition: opacity 0.2s; }
333
+ .node-box {
334
+ stroke-width: 1.5px;
335
+ rx: 8px;
336
+ transition: all 0.2s ease;
337
+ }
338
+ .node-group:hover .node-box {
339
+ stroke-width: 2.5px;
340
+ filter: drop-shadow(0 0 8px rgba(6, 182, 212, 0.4));
341
+ }
342
+ .node-text-title {
343
+ font-family: var(--font-sans);
344
+ font-weight: 700;
345
+ fill: #ffffff;
346
+ font-size: 12px;
347
+ pointer-events: none;
348
+ }
349
+ .node-text-sub {
350
+ font-family: var(--font-sans);
351
+ font-size: 10px;
352
+ fill: #9494a8;
353
+ pointer-events: none;
354
+ }
355
+ .edge-path {
356
+ fill: none;
357
+ stroke-width: 1.5px;
358
+ transition: stroke 0.2s, stroke-width 0.2s;
359
+ }
360
+ .edge-marker { fill: #475569; }
361
+
362
+ /* Dimming & Highlight states */
363
+ .dimmed { opacity: 0.15; }
364
+ .highlighted { opacity: 1 !important; }
365
+ .highlighted-edge { stroke: #06b6d4 !important; stroke-width: 2.5px !important; }
366
+ .blast-ring-1 { stroke: #f43f5e !important; stroke-width: 3px !important; }
367
+ .blast-ring-2 { stroke: #f59e0b !important; stroke-width: 2.5px !important; }
368
+ .blast-ring-3 { stroke: #3b82f6 !important; stroke-width: 2px !important; }
369
+ </style>
370
+ </head>
371
+ <body>
372
+
373
+ <header>
374
+ <div class="brand">
375
+ <span>⚖️ Tribunal Architecture Intelligence</span>
376
+ <span class="brand-badge">TAI Fact Graph</span>
377
+ </div>
378
+
379
+ <div class="projection-tabs">
380
+ <button class="tab-btn active" data-projection="topology">Topology</button>
381
+ <button class="tab-btn" data-projection="security">Security Zones</button>
382
+ <button class="tab-btn" data-projection="failure">Failure Modes</button>
383
+ <button class="tab-btn" data-projection="blast">Blast Radius Simulator</button>
384
+ </div>
385
+
386
+ <div class="header-actions">
387
+ <div class="search-box">
388
+ <span class="search-icon">🔍</span>
389
+ <input type="text" id="node-search" placeholder="Search architecture...">
390
+ </div>
391
+ </div>
392
+ </header>
393
+
394
+ <main class="viewport" id="viewport">
395
+ <svg id="arch-canvas">
396
+ <defs>
397
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
398
+ <path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#64748b" />
399
+ </marker>
400
+ <marker id="arrow-active" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
401
+ <path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#06b6d4" />
402
+ </marker>
403
+ <marker id="arrow-warn" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
404
+ <path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#f59e0b" />
405
+ </marker>
406
+ </defs>
407
+ <g id="world-layer">
408
+ <g id="zones-layer"></g>
409
+ <g id="edges-layer"></g>
410
+ <g id="nodes-layer"></g>
411
+ </g>
412
+ </svg>
413
+
414
+ <div class="canvas-controls">
415
+ <button class="ctrl-btn" id="btn-zoom-in" title="Zoom In">+</button>
416
+ <button class="ctrl-btn" id="btn-zoom-out" title="Zoom Out">−</button>
417
+ <button class="ctrl-btn" id="btn-zoom-reset" title="Fit Screen">⛶</button>
418
+ </div>
419
+
420
+ <div class="legend-card" id="legend">
421
+ <div class="legend-title" id="legend-title">System Topology</div>
422
+ <div id="legend-items"></div>
423
+ </div>
424
+ </main>
425
+
426
+ <aside id="evidence-drawer">
427
+ <button class="drawer-close" id="drawer-close">✕</button>
428
+ <div id="drawer-content"></div>
429
+ </aside>
430
+
431
+ <script id="tribunal-architecture-data" type="application/json">
432
+ ${rawDataJson}
433
+ </script>
434
+
435
+ <script>
436
+ (function () {
437
+ const rawData = JSON.parse(document.getElementById('tribunal-architecture-data').textContent);
438
+ const model = rawData.model;
439
+ const entities = model.entities || [];
440
+ const relationships = model.relationships || [];
441
+ const trustBoundaries = model.trustBoundaries || [];
442
+
443
+ let currentProjection = 'topology';
444
+ let selectedNodeId = null;
445
+
446
+ // Transform & Pan/Zoom State
447
+ let transform = { x: 80, y: 80, k: 0.75 };
448
+ let isPanning = false;
449
+ let startPoint = { x: 0, y: 0 };
450
+
451
+ const viewport = document.getElementById('viewport');
452
+ const world = document.getElementById('world-layer');
453
+ const nodesLayer = document.getElementById('nodes-layer');
454
+ const edgesLayer = document.getElementById('edges-layer');
455
+ const zonesLayer = document.getElementById('zones-layer');
456
+ const drawer = document.getElementById('evidence-drawer');
457
+ const drawerContent = document.getElementById('drawer-content');
458
+ const drawerClose = document.getElementById('drawer-close');
459
+
460
+ // ── Build Spatial Layout with Semantic Column Zoning ──────────────────
461
+ const NODE_W = 160;
462
+ const NODE_H = 60;
463
+ const GAP_X = 240;
464
+ const GAP_Y = 90;
465
+
466
+ const columnMap = {
467
+ endpoint: 0,
468
+ module: 1,
469
+ event_bus: 2,
470
+ queue: 2,
471
+ cache: 2,
472
+ datastore: 3,
473
+ external_api: 3,
474
+ identity_provider: 0,
475
+ test_suite: 1,
476
+ };
477
+
478
+ const columns = [[], [], [], []];
479
+ entities.forEach(e => {
480
+ const col = columnMap[e.kind] ?? 1;
481
+ columns[col].push(e);
482
+ });
483
+
484
+ const nodeCoords = new Map();
485
+ columns.forEach((colEntities, colIdx) => {
486
+ colEntities.forEach((entity, rowIdx) => {
487
+ nodeCoords.set(entity.id, {
488
+ x: colIdx * (NODE_W + GAP_X) + 100,
489
+ y: rowIdx * (NODE_H + GAP_Y) + 100,
490
+ w: NODE_W,
491
+ h: NODE_H,
492
+ entity
493
+ });
494
+ });
495
+ });
496
+
497
+ // ── Render Graph ──────────────────────────────────────────────────────
498
+ function updateCanvasTransform() {
499
+ world.setAttribute('transform', \`translate(\${transform.x}, \${transform.y}) scale(\${transform.k})\`);
500
+ }
501
+
502
+ function renderEdges() {
503
+ edgesLayer.innerHTML = '';
504
+ relationships.forEach(rel => {
505
+ const src = nodeCoords.get(rel.sourceId);
506
+ const tgt = nodeCoords.get(rel.targetId);
507
+ if (!src || !tgt) return;
508
+
509
+ const x1 = src.x + src.w;
510
+ const y1 = src.y + src.h / 2;
511
+ const x2 = tgt.x;
512
+ const y2 = tgt.y + tgt.h / 2;
513
+
514
+ const dx = (x2 - x1) / 2;
515
+ const d = \`M \${x1} \${y1} C \${x1 + dx} \${y1}, \${x2 - dx} \${y2}, \${x2} \${y2}\`;
516
+
517
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
518
+ path.setAttribute('d', d);
519
+ path.setAttribute('class', 'edge-path');
520
+ path.setAttribute('data-id', rel.id);
521
+ path.setAttribute('data-source', rel.sourceId);
522
+ path.setAttribute('data-target', rel.targetId);
523
+
524
+ let strokeColor = '#334155';
525
+ let marker = 'url(#arrow)';
526
+
527
+ if (currentProjection === 'failure' && rel.failureMode && !rel.failureMode.hasTimeout) {
528
+ strokeColor = '#f59e0b';
529
+ marker = 'url(#arrow-warn)';
530
+ }
531
+
532
+ path.setAttribute('stroke', strokeColor);
533
+ path.setAttribute('marker-end', marker);
534
+ edgesLayer.appendChild(path);
535
+ });
536
+ }
537
+
538
+ function renderNodes() {
539
+ nodesLayer.innerHTML = '';
540
+ entities.forEach(entity => {
541
+ const coord = nodeCoords.get(entity.id);
542
+ if (!coord) return;
543
+
544
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
545
+ g.setAttribute('class', 'node-group');
546
+ g.setAttribute('data-id', entity.id);
547
+ g.setAttribute('transform', \`translate(\${coord.x}, \${coord.y})\`);
548
+
549
+ // Color tokens by projection
550
+ let stroke = '#272738';
551
+ let fill = '#14141e';
552
+
553
+ if (currentProjection === 'topology') {
554
+ if (entity.kind === 'endpoint') stroke = '#06b6d4';
555
+ else if (entity.kind === 'datastore' || entity.kind === 'cache') stroke = '#3b82f6';
556
+ else if (entity.kind === 'queue' || entity.kind === 'event_bus') stroke = '#a855f7';
557
+ else if (entity.kind === 'external_api') stroke = '#f43f5e';
558
+ else stroke = '#475569';
559
+ } else if (currentProjection === 'security') {
560
+ if (entity.trustZone === 'public_untrusted') stroke = '#f43f5e';
561
+ else if (entity.trustZone === 'dmz_gateway') stroke = '#f59e0b';
562
+ else if (entity.trustZone === 'internal_service') stroke = '#10b981';
563
+ else if (entity.trustZone === 'isolated_datastore') stroke = '#3b82f6';
564
+ else stroke = '#64748b';
565
+ } else if (currentProjection === 'failure') {
566
+ if (entity.kind === 'external_api') stroke = '#f59e0b';
567
+ else stroke = '#334155';
568
+ }
569
+
570
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
571
+ rect.setAttribute('width', coord.w);
572
+ rect.setAttribute('height', coord.h);
573
+ rect.setAttribute('class', 'node-box');
574
+ rect.setAttribute('fill', fill);
575
+ rect.setAttribute('stroke', stroke);
576
+
577
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
578
+ title.setAttribute('x', 12);
579
+ title.setAttribute('y', 26);
580
+ title.setAttribute('class', 'node-text-title');
581
+ const cleanName = entity.name.length > 18 ? entity.name.slice(0, 16) + '...' : entity.name;
582
+ title.textContent = cleanName;
583
+
584
+ const sub = document.createElementNS('http://www.w3.org/2000/svg', 'text');
585
+ sub.setAttribute('x', 12);
586
+ sub.setAttribute('y', 44);
587
+ sub.setAttribute('class', 'node-text-sub');
588
+ sub.textContent = entity.kind.replace('_', ' ').toUpperCase();
589
+
590
+ g.appendChild(rect);
591
+ g.appendChild(title);
592
+ g.appendChild(sub);
593
+
594
+ g.addEventListener('click', (ev) => {
595
+ ev.stopPropagation();
596
+ onNodeClick(entity.id);
597
+ });
598
+
599
+ nodesLayer.appendChild(g);
600
+ });
601
+ }
602
+
603
+ function updateLegend() {
604
+ const titleEl = document.getElementById('legend-title');
605
+ const itemsEl = document.getElementById('legend-items');
606
+
607
+ const legends = {
608
+ topology: {
609
+ title: 'System Topology',
610
+ items: [
611
+ { color: '#06b6d4', label: 'HTTP API Endpoint' },
612
+ { color: '#475569', label: 'Internal Module' },
613
+ { color: '#3b82f6', label: 'Datastore / Cache' },
614
+ { color: '#a855f7', label: 'Queue / Event Channel' },
615
+ { color: '#f43f5e', label: 'External Service' },
616
+ ]
617
+ },
618
+ security: {
619
+ title: 'Security Trust Zones',
620
+ items: [
621
+ { color: '#f43f5e', label: 'Public Untrusted' },
622
+ { color: '#f59e0b', label: 'DMZ Gateway / Auth' },
623
+ { color: '#10b981', label: 'Authenticated Service' },
624
+ { color: '#3b82f6', label: 'Isolated Datastore' },
625
+ ]
626
+ },
627
+ failure: {
628
+ title: 'Failure Architecture',
629
+ items: [
630
+ { color: '#f59e0b', label: 'External (No Timeout)' },
631
+ { color: '#10b981', label: 'Resilient RPC' },
632
+ ]
633
+ },
634
+ blast: {
635
+ title: 'Blast Radius Simulator',
636
+ items: [
637
+ { color: '#f43f5e', label: 'Ring 1 (Direct Impact)' },
638
+ { color: '#f59e0b', label: 'Ring 2 (Transitive Depth 2)' },
639
+ { color: '#3b82f6', label: 'Ring 3+ (Deep Transitive)' },
640
+ ]
641
+ }
642
+ };
643
+
644
+ const cur = legends[currentProjection] || legends.topology;
645
+ titleEl.textContent = cur.title;
646
+ itemsEl.innerHTML = cur.items.map(it => \`
647
+ <div class="legend-item">
648
+ <div class="legend-dot" style="background: \${it.color}"></div>
649
+ <span>\${it.label}</span>
650
+ </div>
651
+ \`).join('');
652
+ }
653
+
654
+ // ── Interactive Selection & Blast Simulation ─────────────────────────
655
+ function onNodeClick(id) {
656
+ selectedNodeId = id;
657
+ const entity = entities.find(e => e.id === id);
658
+ if (!entity) return;
659
+
660
+ // Reset styling
661
+ document.querySelectorAll('.node-group').forEach(el => el.classList.remove('dimmed', 'highlighted'));
662
+ document.querySelectorAll('.edge-path').forEach(el => {
663
+ el.classList.remove('dimmed', 'highlighted-edge', 'blast-ring-1', 'blast-ring-2', 'blast-ring-3');
664
+ });
665
+
666
+ if (currentProjection === 'blast') {
667
+ // Live Blast Simulation
668
+ const ring1 = new Set();
669
+ const ring2 = new Set();
670
+ const ring3 = new Set();
671
+
672
+ relationships.forEach(r => {
673
+ if (r.targetId === id) ring1.add(r.sourceId);
674
+ });
675
+ relationships.forEach(r => {
676
+ if (ring1.has(r.targetId) && r.sourceId !== id) ring2.add(r.sourceId);
677
+ });
678
+ relationships.forEach(r => {
679
+ if (ring2.has(r.targetId) && !ring1.has(r.sourceId) && r.sourceId !== id) ring3.add(r.sourceId);
680
+ });
681
+
682
+ document.querySelectorAll('.node-group').forEach(el => {
683
+ const elId = el.getAttribute('data-id');
684
+ if (elId === id) el.classList.add('highlighted');
685
+ else if (ring1.has(elId) || ring2.has(elId) || ring3.has(elId)) el.classList.add('highlighted');
686
+ else el.classList.add('dimmed');
687
+ });
688
+
689
+ document.querySelectorAll('.edge-path').forEach(el => {
690
+ const tgt = el.getAttribute('data-target');
691
+ if (tgt === id) el.classList.add('blast-ring-1');
692
+ else if (ring1.has(tgt)) el.classList.add('blast-ring-2');
693
+ else if (ring2.has(tgt)) el.classList.add('blast-ring-3');
694
+ else el.classList.add('dimmed');
695
+ });
696
+ } else {
697
+ // Standard connected focus
698
+ const connectedNodes = new Set([id]);
699
+ relationships.forEach(r => {
700
+ if (r.sourceId === id) connectedNodes.add(r.targetId);
701
+ if (r.targetId === id) connectedNodes.add(r.sourceId);
702
+ });
703
+
704
+ document.querySelectorAll('.node-group').forEach(el => {
705
+ const elId = el.getAttribute('data-id');
706
+ if (connectedNodes.has(elId)) el.classList.add('highlighted');
707
+ else el.classList.add('dimmed');
708
+ });
709
+
710
+ document.querySelectorAll('.edge-path').forEach(el => {
711
+ const s = el.getAttribute('data-source');
712
+ const t = el.getAttribute('data-target');
713
+ if (s === id || t === id) el.classList.add('highlighted-edge');
714
+ else el.classList.add('dimmed');
715
+ });
716
+ }
717
+
718
+ openDrawer(entity);
719
+ }
720
+
721
+ function openDrawer(entity) {
722
+ drawer.classList.add('open');
723
+ const sources = entity.sources || [];
724
+
725
+ drawerContent.innerHTML = \`
726
+ <h2 class="drawer-title">\${entity.name}</h2>
727
+ <div class="drawer-subtitle">\${entity.id}</div>
728
+
729
+ <div class="stat-pill-group">
730
+ <div class="stat-pill">Kind: <strong>\${entity.kind}</strong></div>
731
+ <div class="stat-pill">Zone: <strong>\${entity.trustZone || 'none'}</strong></div>
732
+ <div class="stat-pill">Risk: <strong>\${entity.blastRadius?.riskScore || 0.1}</strong></div>
733
+ <div class="stat-pill">Confidence: <span class="confidence-badge conf-\${entity.confidence || 'L1'}">\${entity.confidence || 'L1'} FACT</span></div>
734
+ </div>
735
+
736
+ <div class="section-title">Verified Source Code Evidence</div>
737
+ \${sources.map(s => \`
738
+ <div class="evidence-block">
739
+ <div class="evidence-loc">\${s.file}:\${s.startLine}\${s.endLine ? '-' + s.endLine : ''}</div>
740
+ <div>Hash: <code>\${s.contentHash || 'verified'}</code></div>
741
+ \${s.snippet ? \`<div class="evidence-snippet">\${s.snippet}</div>\` : ''}
742
+ </div>
743
+ \`).join('')}
744
+
745
+ <div class="section-title">Architectural Role & Description</div>
746
+ <p style="font-size: 13px; line-height: 1.5; color: #cbd5e1; margin-bottom: 16px;">
747
+ \${entity.role || 'Internal architectural component extracted deterministically from codebase AST.'}
748
+ </p>
749
+ \`;
750
+ }
751
+
752
+ drawerClose.addEventListener('click', () => {
753
+ drawer.classList.remove('open');
754
+ document.querySelectorAll('.node-group').forEach(el => el.classList.remove('dimmed', 'highlighted'));
755
+ document.querySelectorAll('.edge-path').forEach(el => {
756
+ el.classList.remove('dimmed', 'highlighted-edge', 'blast-ring-1', 'blast-ring-2', 'blast-ring-3');
757
+ });
758
+ });
759
+
760
+ // ── Event Handlers: Pan / Zoom / Tabs ────────────────────────────────
761
+ viewport.addEventListener('mousedown', (e) => {
762
+ if (e.target.closest('.node-group')) return;
763
+ isPanning = true;
764
+ startPoint = { x: e.clientX - transform.x, y: e.clientY - transform.y };
765
+ });
766
+ window.addEventListener('mousemove', (e) => {
767
+ if (!isPanning) return;
768
+ transform.x = e.clientX - startPoint.x;
769
+ transform.y = e.clientY - startPoint.y;
770
+ updateCanvasTransform();
771
+ });
772
+ window.addEventListener('mouseup', () => { isPanning = false; });
773
+
774
+ viewport.addEventListener('wheel', (e) => {
775
+ e.preventDefault();
776
+ const factor = e.deltaY > 0 ? 0.9 : 1.1;
777
+ transform.k = Math.max(0.2, Math.min(3.0, transform.k * factor));
778
+ updateCanvasTransform();
779
+ });
780
+
781
+ document.getElementById('btn-zoom-in').addEventListener('click', () => {
782
+ transform.k = Math.min(3.0, transform.k * 1.2);
783
+ updateCanvasTransform();
784
+ });
785
+ document.getElementById('btn-zoom-out').addEventListener('click', () => {
786
+ transform.k = Math.max(0.2, transform.k / 1.2);
787
+ updateCanvasTransform();
788
+ });
789
+ document.getElementById('btn-zoom-reset').addEventListener('click', () => {
790
+ transform = { x: 80, y: 80, k: 0.75 };
791
+ updateCanvasTransform();
792
+ });
793
+
794
+ document.querySelectorAll('.tab-btn').forEach(btn => {
795
+ btn.addEventListener('click', () => {
796
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
797
+ btn.classList.add('active');
798
+ currentProjection = btn.getAttribute('data-projection');
799
+ updateLegend();
800
+ renderNodes();
801
+ renderEdges();
802
+ if (selectedNodeId) onNodeClick(selectedNodeId);
803
+ });
804
+ });
805
+
806
+ // Search Handler
807
+ document.getElementById('node-search').addEventListener('input', (e) => {
808
+ const query = e.target.value.toLowerCase().trim();
809
+ if (!query) {
810
+ document.querySelectorAll('.node-group').forEach(el => el.classList.remove('dimmed', 'highlighted'));
811
+ return;
812
+ }
813
+ document.querySelectorAll('.node-group').forEach(el => {
814
+ const id = el.getAttribute('data-id').toLowerCase();
815
+ const ent = entities.find(x => x.id.toLowerCase() === id);
816
+ if (id.includes(query) || (ent && ent.name.toLowerCase().includes(query))) {
817
+ el.classList.add('highlighted');
818
+ el.classList.remove('dimmed');
819
+ } else {
820
+ el.classList.add('dimmed');
821
+ el.classList.remove('highlighted');
822
+ }
823
+ });
824
+ });
825
+
826
+ // Initial Mount
827
+ updateCanvasTransform();
828
+ updateLegend();
829
+ renderNodes();
830
+ renderEdges();
831
+ })();
832
+ </script>
833
+ </body>
834
+ </html>`;
835
+ }
836
+
837
+ saveHtml(outputPath) {
838
+ const dest =
839
+ outputPath || path.join(process.cwd(), '.agent', 'history', 'architecture-intelligence.html');
840
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
841
+ const html = this.generateHtml();
842
+ fs.writeFileSync(dest, html, 'utf8');
843
+ return dest;
844
+ }
845
+ }
846
+
847
+ // ── Standalone CLI ────────────────────────────────────────────────────────────
848
+ if (require.main === module) {
849
+ const t = timer();
850
+ const repoRoot = process.cwd();
851
+ const modelFile = path.join(repoRoot, '.agent', 'history', 'architecture-model.json');
852
+ const verifFile = path.join(repoRoot, '.agent', 'history', 'architecture-verification.json');
853
+
854
+ console.log(banner('Tribunal Architecture Intelligence Visualizer'));
855
+
856
+ if (!fs.existsSync(modelFile)) {
857
+ console.error(
858
+ ` ${RED}✖ architecture-model.json not found.${RESET} Run architecture_extractor.js first.`,
859
+ );
860
+ process.exit(1);
861
+ }
862
+
863
+ const model = JSON.parse(fs.readFileSync(modelFile, 'utf8'));
864
+ const verification = fs.existsSync(verifFile)
865
+ ? JSON.parse(fs.readFileSync(verifFile, 'utf8'))
866
+ : null;
867
+
868
+ const visualizer = new ArchitectureVisualizer(model, verification);
869
+ const outPath = visualizer.saveHtml();
870
+
871
+ console.log(` ${GREEN}${BOX.check} Interactive Architecture Explorer Generated!${RESET}`);
872
+ console.log(` ${DIM}File:${RESET} ${CYAN}${outPath}${RESET} (${formatMs(t())})`);
873
+ console.log(
874
+ ` ${DIM}Projections Enabled:${RESET} Topology, Security Zones, Failure Modes, Blast Simulator\n`,
875
+ );
876
+ }
877
+
878
+ module.exports = {
879
+ ArchitectureVisualizer,
880
+ };