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.
- package/.agent/ARCHITECTURE.md +2 -2
- package/.agent/history/architecture-intelligence.html +1487 -401
- package/.agent/history/architecture-model.json +9365 -5697
- package/.agent/history/architecture-verification.json +37 -9
- package/.agent/routing_index.json +2069 -3340
- package/.agent/rules/GEMINI.md +1 -1
- package/.agent/scripts/adaptive_execution.js +511 -0
- package/.agent/scripts/architecture_diff.js +303 -260
- package/.agent/scripts/architecture_extractor.js +792 -753
- package/.agent/scripts/architecture_health.js +293 -256
- package/.agent/scripts/architecture_intelligence.js +3100 -0
- package/.agent/scripts/architecture_verifier.js +303 -270
- package/.agent/scripts/architecture_visualizer.js +880 -873
- package/.agent/scripts/blast_radius_engine.js +274 -234
- package/.agent/scripts/capability_evolution.js +453 -0
- package/.agent/scripts/concept_extractor.js +240 -0
- package/.agent/scripts/consideration_engine.js +284 -0
- package/.agent/scripts/coverage_engine.js +102 -0
- package/.agent/scripts/cross_domain_engine.js +1883 -0
- package/.agent/scripts/engineering_memory.js +1313 -0
- package/.agent/scripts/integrity_manifest.js +1 -5
- package/.agent/scripts/judgment_engine.js +790 -0
- package/.agent/scripts/materiality_engine.js +251 -0
- package/.agent/scripts/outcome_engine.js +624 -0
- package/.agent/scripts/outcome_optimization_engine.js +1632 -0
- package/.agent/scripts/simulation_engine.js +1478 -0
- package/.agent/scripts/skill_intelligence.js +1371 -0
- package/.agent/scripts/validation_engine.js +236 -236
- package/.agent/scripts/verification_auditor.js +266 -0
- package/.agent/skill-intelligence/taxonomy/engineering-concepts.json +555 -0
- package/.agent/skill_topic_map.json +301 -341
- package/.agent/skills_inventory.json +4272 -6922
- package/.agent/workflows/arch.md +96 -80
- package/.agent/workflows/skill-intelligence.md +216 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +24 -24
- package/bin/adapter-install.js +1 -1
- package/package.json +8 -9
- package/plugins/core-plugin.json +1 -1
- package/plugins/gemini/extension.json +1 -1
- package/src/browser/index.js +2 -1
- package/src/browser/launcher.js +41 -1
- package/src/cli.js +3 -1
- package/src/commands/arch.js +261 -172
- package/src/commands/context.js +27 -8
- package/src/commands/learn.js +21 -6
- package/src/commands/memory.js +10 -1
- package/src/commands/native.js +12 -3
- package/src/commands/skill-intelligence.js +1165 -0
- package/src/commands/system1.js +105 -4
- package/src/context/ranker.js +337 -339
- package/src/evolution/engine.js +296 -295
- package/src/synthesis/claim_check_separator.js +294 -271
- package/src/system1/browser_intelligence.js +300 -0
- package/src/system1/constants.js +49 -0
- package/src/system1/decision_engine.js +498 -0
- package/src/system1/evidence_engine.js +602 -0
- package/src/system1/evolution_pipeline.js +581 -0
- package/src/system1/index.d.ts +114 -0
- package/src/system1/index.js +102 -0
- package/src/system1/reviewer_orchestrator.js +477 -0
- package/src/tui/banner.js +1 -1
- 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 =
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
+
};
|