betterwithage commited on
Commit
15806fd
·
verified ·
1 Parent(s): 9ad788e

chore(sync): mirror static/3d kit + Dockerfile-COPY'd web/*.html to Space (hf-sync)

Browse files

Automated 3D/web-page asset sync from szl-holdings/a11oy main via hf-sync.
web/*.html set parsed live from the Dockerfile COPY directives.
Added/updated: 53 file(s)
Deleted (gone from GitHub main): (none)

Keeps static/3d/** and every Dockerfile-COPY'd web/<page>.html identical
to GitHub main so an HF factory rebuild never drops a GitHub edit (closes
the chronic hf-module-drift red for the 3D kit + baked web pages).

Files changed (1) hide show
  1. static/3d/surfaces/governance.js +706 -59
static/3d/surfaces/governance.js CHANGED
@@ -1,77 +1,724 @@
1
  // SPDX-License-Identifier: Apache-2.0
2
- // © 2026 Lutar, Stephen P. Jr. — SZL Holdings · Doctrine v11
3
  //
4
- // surfaces/governance.js — AI Governance surface STUB (Dev0 foundation; Dev5 fills this).
5
  //
6
- // Leader/technique to model: GUAC + Sigstore/Rekor
7
- // Primary live endpoint (doctrine: WIRE TO LIVE DATA, never fabricate): /api/a11oy/v1/assurance/artifact
 
 
 
 
8
  //
9
- // CONTRACT every surface module is an ES module default-exporting:
10
- // { id, title, endpoints[], mount(ctx), unmount() }
11
- // ctx (provided by the /holographic shell via szl3d):
12
- // ctx.stage Stage from szl3d_boot.boot() scene, camera, renderer, THREE,
13
- // start, stop, onFrame, setBloom, backend, ...
14
- // ctx.container the surface's DOM panel (for overlays/badges/HUD)
15
- // ctx.live the szl3d_live module (ctx.live.poll(endpoint, ms, onData, {badge}))
16
- // ctx.label the szl3d_label module (ctx.label.chip / .billboard / .legend)
17
- // ctx.THREE the three module
18
- // mount() attaches scene objects + starts polling. unmount() stops polls and removes
19
- // DOM it added (the shell disposes the stage).
20
  //
21
- // This STUB renders an honest "awaiting Dev5" placeholder + a LIVE badge wired to the
22
- // real endpoint, proving the toolkit primitives work end-to-end before the real viz lands.
 
 
 
 
 
 
 
23
 
24
  const ID = "governance";
25
- const TITLE = "AI Governance";
26
- const ENDPOINT = "/api/a11oy/v1/assurance/artifact";
27
- const ACCENT = 0xb08fff;
28
 
29
- let _stage = null, _handle = null, _overlay = null, _obj = null;
 
 
 
 
 
 
 
 
30
 
31
- function mount(ctx) {
32
- _stage = ctx.stage;
33
- const THREE = ctx.THREE;
 
 
 
 
34
 
35
- const geo = new THREE.IcosahedronGeometry(2.4, 1);
36
- const mat = new THREE.MeshStandardMaterial({
37
- color: ACCENT, emissive: ACCENT, emissiveIntensity: 0.35,
38
- metalness: 0.4, roughness: 0.35, wireframe: true,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  });
40
- _obj = new THREE.Mesh(geo, mat);
41
- _stage.scene.add(_obj);
42
- _stage.onFrame(() => { if (_obj) { _obj.rotation.y += 0.004; _obj.rotation.x += 0.0015; } });
43
-
44
- try {
45
- const bb = ctx.label.billboard(THREE, "STRUCTURAL-ONLY", { text: TITLE, scale: 0.7, position: [0, 3.4, 0] });
46
- _stage.scene.add(bb);
47
- } catch (_) {}
48
-
49
- _overlay = document.createElement("div");
50
- _overlay.className = "szl3d-surface-overlay";
51
- Object.assign(_overlay.style, { position: "absolute", left: "14px", top: "14px", zIndex: "5",
52
- display: "flex", flexDirection: "column", gap: "8px", maxWidth: "min(92%,420px)" });
53
- const h = document.createElement("div");
54
- h.style.cssText = "font:600 13px ui-sans-serif,system-ui;color:#eef3f6;letter-spacing:.4px";
55
- h.textContent = TITLE + " · awaiting Dev5";
56
- const badge = ctx.live.createBadge();
57
- const legend = ctx.label.legend();
58
- legend.style.opacity = "0.85";
59
- _overlay.appendChild(h); _overlay.appendChild(badge.el); _overlay.appendChild(legend);
60
- (ctx.container || document.body).appendChild(_overlay);
61
-
62
- _handle = ctx.live.poll(ENDPOINT, 5000, (json, meta) => {
63
- // Dev5: replace with the real viz update. Honesty label is in meta.label.
64
- if (meta.label && _obj) { /* map live values to geometry here */ }
65
- }, { badge });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  return { id: ID, started: true };
68
  }
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  function unmount() {
71
- try { if (_handle) _handle.stop(); } catch (_) {}
72
- try { if (_overlay && _overlay.parentNode) _overlay.parentNode.removeChild(_overlay); } catch (_) {}
73
- try { if (_obj && _stage) _stage.scene.remove(_obj); } catch (_) {}
74
- _handle = null; _overlay = null; _obj = null; _stage = null;
 
 
 
 
 
 
 
 
75
  }
76
 
77
- export default { id: ID, title: TITLE, endpoints: [ENDPOINT], mount, unmount };
 
1
  // SPDX-License-Identifier: Apache-2.0
2
+ // © 2026 Lutar, Stephen P. Jr. — SZL Holdings · ORCID 0009-0001-0110-4173 · Doctrine v11
3
  //
4
+ // surfaces/governance.js — AI GOVERNANCE / Assurance holographic surface (Dev5).
5
  //
6
+ // Leader/technique modeled (we are MODELED ON, never claim to BE):
7
+ // GUAC v1.0 (OpenSSF) Graph for Understanding Artifact Composition
8
+ // Sigstore / Rekor — append-only hash-chained Merkle transparency log
9
+ // SCITT (IETF) — Transparent Statement -> Ledger Entry -> Merkle proof
10
+ // Technique: 3D force-directed dependency knowledge graph + 3D Merkle hash-chain
11
+ // tree + compliance crosswalk heatmap.
12
  //
13
+ // DOCTRINE v11 (LOCKED): WIRE TO LIVE DATA, never fabricate. Every value on screen
14
+ // traces to a real a11oy endpoint and carries its honesty label read straight off the
15
+ // JSON. The 5 assurance/forge routes are PENDING the Forge mesh deploy and currently
16
+ // 404 -> ctx.live.poll renders the honest NO-LIVE-DATA badge for each; this viz is
17
+ // built to the REAL data shapes the engines (artifact_behaviour_monitor.py,
18
+ // content_credentials.py, compliance_crosswalk.py, runtime_attestation.py,
19
+ // forge_governance.py, pq_signing.py) WILL return, so it lights up automatically on 200.
 
 
 
 
20
  //
21
+ // TEACHING POINT (doctrine): a SIGNATURE is NOT proof of safety CVE-2026-45321
22
+ // (Mini Shai-Hulud): 84 @tanstack/* npm versions shipped VALID SLSA L3 / Sigstore
23
+ // provenance; provenance flagged ZERO; behaviour caught all 84. Λ = Conjecture 1
24
+ // (advisory governance, NOT proven trust / NOT proven safe).
25
+ //
26
+ // 0 runtime CDN: three via the page importmap (ctx.THREE); the SBOM force-graph reuses
27
+ // the repo-vendored 3d-force-graph UMD global at /vendor/3d-force-graph.min.js.
28
+ //
29
+ // CONTRACT: default-export { id, title, endpoints[], mount(ctx), unmount() }.
30
 
31
  const ID = "governance";
32
+ const TITLE = "AI Governance · Assurance";
 
 
33
 
34
+ // The 5 gap routes (PENDING Forge mesh; 404 -> NO-LIVE-DATA until 200).
35
+ const EP = {
36
+ artifact: "/api/a11oy/v1/assurance/artifact", // artifact_behaviour_monitor.py
37
+ credential: "/api/a11oy/v1/assurance/credential", // content_credentials.py (C2PA)
38
+ compliance: "/api/a11oy/v1/assurance/compliance", // compliance_crosswalk.py + compliance.json
39
+ attest: "/api/a11oy/v1/assurance/attest", // runtime_attestation.py
40
+ ledger: "/api/a11oy/v1/forge/ledger", // forge_governance.py
41
+ };
42
+ const ENDPOINTS = Object.values(EP);
43
 
44
+ // Palette (matches the holographic shell tokens).
45
+ const C = {
46
+ accent: 0xb08fff, teal: 0x39d3c4, gold: 0xe8c074, red: 0xff6b6b,
47
+ green: 0x2fd07a, gray: 0x8a97a3, blue: 0x6fb1ff, cream: 0xeef3f6,
48
+ };
49
+ // Honest framework-status colors (IMPLEMENTED/PARTIAL/ROADMAP) for the heatmap.
50
+ const STATUS_COLOR = { IMPLEMENTED: 0x2fd07a, PARTIAL: 0xe8c074, ROADMAP: 0x8a97a3 };
51
 
52
+ let _stage = null, _THREE = null, _label = null, _live = null;
53
+ let _root = null; // THREE.Group holding everything we add to the scene
54
+ let _handles = []; // poll handles to stop on unmount
55
+ let _overlay = null, _graphPanel = null, _graph = null, _hud = {};
56
+ let _frameCb = null, _fgScript = null;
57
+ const _state = {}; // last meta per endpoint (for HUD)
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // small helpers
61
+ // ---------------------------------------------------------------------------
62
+ function el(tag, css, txt) {
63
+ const e = document.createElement(tag);
64
+ if (css) e.style.cssText = css;
65
+ if (txt != null) e.textContent = txt;
66
+ return e;
67
+ }
68
+ function disposeObj(o) {
69
+ o.traverse?.((c) => {
70
+ if (c.geometry) c.geometry.dispose?.();
71
+ if (c.material) {
72
+ const m = c.material;
73
+ (Array.isArray(m) ? m : [m]).forEach((mm) => { mm.map?.dispose?.(); mm.dispose?.(); });
74
+ }
75
  });
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // mount
80
+ // ---------------------------------------------------------------------------
81
+ function mount(ctx) {
82
+ _stage = ctx.stage; _THREE = ctx.THREE; _label = ctx.label; _live = ctx.live;
83
+ const THREE = _THREE;
84
+ _root = new THREE.Group();
85
+ _stage.scene.add(_root);
86
+
87
+ _buildOverlay(ctx);
88
+
89
+ // --- the 7 in-scene scene demos (positioned around the estate) -----------
90
+ const merkle = buildMerkleTree(THREE); // Demo: Rekor-style 3D Merkle hash-chain tree
91
+ merkle.position.set(-7.5, 0, 0);
92
+ _root.add(merkle);
93
+
94
+ const helix = buildAttestationHelix(THREE); // Demo: attestation timeline helix
95
+ helix.position.set(7.5, -1, 0);
96
+ _root.add(helix);
97
+
98
+ const heatmap = buildComplianceHeatmap(THREE); // Demo: NIST/ISO/EU compliance heatmap (60/60/0)
99
+ heatmap.position.set(0, 4.2, -3);
100
+ _root.add(heatmap);
101
+
102
+ const ledger = buildLedgerChain(THREE); // Demo: Forge ledger hash-chain replay
103
+ ledger.position.set(0, -4.4, 0);
104
+ _root.add(ledger);
105
+
106
+ const axes = buildAttestationAxes(THREE); // Demo: 3-axis attestation (build/model/runtime)
107
+ axes.position.set(7.0, 4.0, -1);
108
+ _root.add(axes);
109
+
110
+ const pq = buildPqNode(THREE); // Demo: PQ hybrid Ed25519+ML-DSA node
111
+ pq.position.set(-7.5, 4.0, -1);
112
+ _root.add(pq);
113
+
114
+ const killSwitch = buildKillSwitch(THREE); // Demo: kill-switch indicator
115
+ killSwitch.position.set(0, 0, 6);
116
+ _root.add(killSwitch);
117
+
118
+ const callout = buildSigSafetyCallout(THREE); // Demo: signature ≠ safety teaching callout
119
+ callout.position.set(0, -8.2, 2);
120
+ _root.add(callout);
121
+
122
+ // doctrine billboards (every cluster is labelled honestly until its route is live)
123
+ _root.add(_label.billboard(THREE, "STRUCTURAL-ONLY", { text: "Merkle ledger", scale: 0.5, position: [-7.5, 4.4, 0] }));
124
+ _root.add(_label.billboard(THREE, "STRUCTURAL-ONLY", { text: "Attest helix", scale: 0.5, position: [7.5, 3.0, 0] }));
125
+ _root.add(_label.billboard(THREE, "MEASURED", { text: "Crosswalk 60/60/0", scale: 0.5, position: [0, 7.0, -3] }));
126
+
127
+ this_registerFrame(THREE, { merkle, helix, heatmap, ledger, axes, pq, killSwitch, callout });
128
+
129
+ // --- SBOM dependency force-graph (GUAC-style), reuses vendored ForceGraph3D
130
+ _initForceGraph();
131
+
132
+ // --- wire all 5 gap routes; each renders NO-LIVE-DATA honestly on 404 -----
133
+ startPolls(ctx);
134
 
135
  return { id: ID, started: true };
136
  }
137
 
138
+ // per-frame animation registered once with the stage.
139
+ function this_registerFrame(THREE, parts) {
140
+ let t = 0;
141
+ _frameCb = () => {
142
+ t += 0.016;
143
+ if (_root) _root.rotation.y = Math.sin(t * 0.08) * 0.06;
144
+ parts.helix.rotation.y += 0.006;
145
+ parts.merkle.rotation.y += 0.003;
146
+ parts.ledger.rotation.y -= 0.004;
147
+ // kill-switch pulse (armed=red glow throb; safe=steady green) — value set by poll
148
+ const ks = parts.killSwitch.userData;
149
+ if (ks.core) {
150
+ const armed = !!ks.armed;
151
+ const pulse = armed ? (0.5 + 0.5 * Math.sin(t * 6)) : 0.85;
152
+ ks.core.material.emissiveIntensity = (armed ? 0.6 : 0.3) + pulse * (armed ? 0.8 : 0.1);
153
+ }
154
+ // PQ node: two interlocked rings (Ed25519 + ML-DSA) counter-rotate
155
+ if (parts.pq.userData.ed) parts.pq.userData.ed.rotation.z += 0.01;
156
+ if (parts.pq.userData.ml) parts.pq.userData.ml.rotation.x -= 0.012;
157
+ // helix beads orbit on flowing
158
+ (parts.helix.userData.beads || []).forEach((b, i) => {
159
+ b.material.emissiveIntensity = 0.4 + 0.4 * Math.sin(t * 2 + i * 0.7);
160
+ });
161
+ };
162
+ _stage.onFrame(_frameCb);
163
+ }
164
+
165
+ // ===========================================================================
166
+ // DEMOS — 3D scene objects (built to the engine shapes; live values overlaid)
167
+ // ===========================================================================
168
+
169
+ // Demo 1: Rekor/SCITT-style 3D Merkle hash-chain tree (TubeGeometry branches).
170
+ // forge ledger uses a LINEAR khipu chain (prev_hash/entry_hash, no merkle); the
171
+ // Rekor inclusion proof is a binary Merkle tree — we render the canonical Merkle
172
+ // structure and color leaves by signature freshness once /forge/ledger is live.
173
+ function buildMerkleTree(THREE) {
174
+ const g = new THREE.Group(); g.userData.kind = "merkle";
175
+ const leafMat = () => new THREE.MeshStandardMaterial({
176
+ color: C.gray, emissive: C.gray, emissiveIntensity: 0.25, metalness: 0.4, roughness: 0.4,
177
+ });
178
+ const levels = 4; // 1 + 2 + 4 + 8 = 15 nodes
179
+ const nodes = [];
180
+ let y = 3.2;
181
+ const spread = 4.6;
182
+ const branchMat = new THREE.MeshStandardMaterial({ color: C.accent, emissive: C.accent, emissiveIntensity: 0.2, transparent: true, opacity: 0.55 });
183
+ const placed = [];
184
+ for (let lvl = 0; lvl < levels; lvl++) {
185
+ const count = Math.pow(2, lvl);
186
+ const row = [];
187
+ const w = spread * (lvl / (levels - 1));
188
+ for (let i = 0; i < count; i++) {
189
+ const x = count === 1 ? 0 : (-w + (2 * w) * (i / (count - 1)));
190
+ const isLeaf = lvl === levels - 1;
191
+ const geo = new THREE.OctahedronGeometry(isLeaf ? 0.28 : 0.36, 0);
192
+ const node = new THREE.Mesh(geo, leafMat());
193
+ node.position.set(x, y - lvl * 1.7, 0);
194
+ node.userData.leaf = isLeaf;
195
+ g.add(node); row.push(node); nodes.push(node);
196
+ }
197
+ placed.push(row);
198
+ }
199
+ // connect parents -> children with tube branches
200
+ for (let lvl = 0; lvl < levels - 1; lvl++) {
201
+ placed[lvl].forEach((parent, pi) => {
202
+ [2 * pi, 2 * pi + 1].forEach((ci) => {
203
+ const child = placed[lvl + 1][ci];
204
+ if (!child) return;
205
+ const curve = new THREE.LineCurve3(parent.position.clone(), child.position.clone());
206
+ const tube = new THREE.Mesh(new THREE.TubeGeometry(curve, 1, 0.03, 6, false), branchMat);
207
+ g.add(tube);
208
+ });
209
+ });
210
+ }
211
+ g.userData.leaves = placed[levels - 1];
212
+ g.userData.root = placed[0][0];
213
+ g.userData.allNodes = nodes;
214
+ return g;
215
+ }
216
+
217
+ // Demo 2: attestation timeline helix — SCITT-style. Each ring = a pipeline stage;
218
+ // beads = attestations (SLSA provenance, SBOM, test result) that lock into the ledger.
219
+ function buildAttestationHelix(THREE) {
220
+ const g = new THREE.Group(); g.userData.kind = "helix";
221
+ const turns = 3, perTurn = 12, n = turns * perTurn, R = 1.7, H = 6;
222
+ const pts = [];
223
+ for (let i = 0; i <= n; i++) {
224
+ const a = (i / perTurn) * Math.PI * 2;
225
+ const yy = -H / 2 + H * (i / n);
226
+ pts.push(new THREE.Vector3(Math.cos(a) * R, yy, Math.sin(a) * R));
227
+ }
228
+ const curve = new THREE.CatmullRomCurve3(pts);
229
+ const tube = new THREE.Mesh(
230
+ new THREE.TubeGeometry(curve, 200, 0.045, 8, false),
231
+ new THREE.MeshStandardMaterial({ color: C.teal, emissive: C.teal, emissiveIntensity: 0.3, transparent: true, opacity: 0.7 }),
232
+ );
233
+ g.add(tube);
234
+ const beads = [];
235
+ const stageColors = [C.gold, C.teal, C.accent, C.blue];
236
+ for (let i = 0; i < n; i += 3) {
237
+ const p = curve.getPointAt(i / n);
238
+ const bead = new THREE.Mesh(
239
+ new THREE.SphereGeometry(0.12, 12, 12),
240
+ new THREE.MeshStandardMaterial({ color: stageColors[(i / 3) % stageColors.length], emissive: stageColors[(i / 3) % stageColors.length], emissiveIntensity: 0.5 }),
241
+ );
242
+ bead.position.copy(p); g.add(bead); beads.push(bead);
243
+ }
244
+ g.userData.beads = beads;
245
+ return g;
246
+ }
247
+
248
+ // Demo 3: NIST / ISO / EU compliance crosswalk heatmap (HONEST 60/60/0).
249
+ // Rows = controls (compliance.json), cols = the 3 frameworks. Cell color =
250
+ // IMPLEMENTED/PARTIAL/ROADMAP; bar height = framework pct_implemented. We seed
251
+ // it with the STRUCTURAL frame from compliance.json and replace with live cells
252
+ // when /assurance/compliance is 200.
253
+ function buildComplianceHeatmap(THREE) {
254
+ const g = new THREE.Group(); g.userData.kind = "heatmap";
255
+ const frameworks = ["NIST", "ISO", "EU"];
256
+ const rows = 10, cols = 3, cw = 1.05, ch = 0.5;
257
+ const cells = [];
258
+ // STRUCTURAL seed mirroring compliance.json (NIST/ISO: 6 impl,3 part,1 road; EU: 0,9,1)
259
+ const seed = {
260
+ NIST: ["IMPLEMENTED", "IMPLEMENTED", "IMPLEMENTED", "IMPLEMENTED", "IMPLEMENTED", "IMPLEMENTED", "PARTIAL", "PARTIAL", "PARTIAL", "ROADMAP"],
261
+ ISO: ["IMPLEMENTED", "IMPLEMENTED", "IMPLEMENTED", "IMPLEMENTED", "IMPLEMENTED", "IMPLEMENTED", "PARTIAL", "PARTIAL", "PARTIAL", "ROADMAP"],
262
+ EU: ["PARTIAL", "PARTIAL", "PARTIAL", "PARTIAL", "PARTIAL", "PARTIAL", "PARTIAL", "PARTIAL", "PARTIAL", "ROADMAP"],
263
+ };
264
+ for (let c = 0; c < cols; c++) {
265
+ for (let r = 0; r < rows; r++) {
266
+ const status = seed[frameworks[c]][r];
267
+ const mesh = new THREE.Mesh(
268
+ new THREE.BoxGeometry(cw * 0.9, 0.12, ch * 0.9),
269
+ new THREE.MeshStandardMaterial({ color: STATUS_COLOR[status], emissive: STATUS_COLOR[status], emissiveIntensity: 0.3, metalness: 0.2, roughness: 0.6 }),
270
+ );
271
+ mesh.position.set((c - 1) * cw, 0, (r - rows / 2) * ch);
272
+ mesh.userData = { framework: frameworks[c], row: r };
273
+ g.add(mesh); cells.push(mesh);
274
+ }
275
+ }
276
+ // framework coverage pillars (height = pct_implemented; seeded 60/60/0)
277
+ const pct = { NIST: 60, ISO: 60, EU: 0 };
278
+ const pillars = {};
279
+ frameworks.forEach((f, c) => {
280
+ const h = Math.max(0.05, (pct[f] / 100) * 3.5);
281
+ const col = pct[f] >= 100 ? C.green : (pct[f] > 0 ? C.gold : C.gray);
282
+ const pil = new THREE.Mesh(
283
+ new THREE.BoxGeometry(0.5, h, 0.5),
284
+ new THREE.MeshStandardMaterial({ color: col, emissive: col, emissiveIntensity: 0.35, transparent: true, opacity: 0.85 }),
285
+ );
286
+ pil.position.set((c - 1) * cw, h / 2 + 0.2, (rows / 2) * ch + 0.9);
287
+ g.add(pil); pillars[f] = pil;
288
+ const bb = _label.billboard(THREE, "MEASURED", { text: `${f} ${pct[f]}%`, scale: 0.34, position: [(c - 1) * cw, h + 0.7, (rows / 2) * ch + 0.9] });
289
+ g.add(bb);
290
+ });
291
+ g.userData.cells = cells; g.userData.pillars = pillars; g.userData.frameworks = frameworks;
292
+ return g;
293
+ }
294
+
295
+ // Demo 4: Forge ledger hash-chain replay — linear khipu chain (prev_hash/entry_hash).
296
+ // Genesis = 64 zeros; ring color (safe-auto/gated/forbidden) + decision (ALLOW/DENY/
297
+ // BLOCKED). Populated from /forge/ledger entries[]; kill_switch drives the indicator.
298
+ function buildLedgerChain(THREE) {
299
+ const g = new THREE.Group(); g.userData.kind = "ledger";
300
+ const n = 8, gap = 1.25;
301
+ const blocks = [];
302
+ for (let i = 0; i < n; i++) {
303
+ const blk = new THREE.Mesh(
304
+ new THREE.BoxGeometry(0.7, 0.7, 0.7),
305
+ new THREE.MeshStandardMaterial({ color: C.gray, emissive: C.gray, emissiveIntensity: 0.25, metalness: 0.5, roughness: 0.4 }),
306
+ );
307
+ blk.position.set((i - (n - 1) / 2) * gap, 0, 0);
308
+ g.add(blk); blocks.push(blk);
309
+ if (i > 0) {
310
+ const link = new THREE.Mesh(
311
+ new THREE.CylinderGeometry(0.025, 0.025, gap - 0.7, 6),
312
+ new THREE.MeshStandardMaterial({ color: C.accent, emissive: C.accent, emissiveIntensity: 0.3 }),
313
+ );
314
+ link.rotation.z = Math.PI / 2;
315
+ link.position.set((i - (n - 1) / 2) * gap - gap / 2, 0, 0);
316
+ g.add(link);
317
+ }
318
+ }
319
+ // genesis marker (64 zeros)
320
+ blocks[0].material.color.setHex(C.blue); blocks[0].material.emissive.setHex(C.blue);
321
+ g.userData.blocks = blocks;
322
+ return g;
323
+ }
324
+
325
+ // Demo 5: 3-axis attestation (build / model / runtime) — runtime_attestation.py.
326
+ // Three orthogonal axes; a bar grows along each present axis. trust_level rises
327
+ // NONE -> BUILD-ONLY -> BUILD+MODEL -> ALL-THREE-AXES. More axes = more STRUCTURE,
328
+ // NOT more safety (lambda_note).
329
+ function buildAttestationAxes(THREE) {
330
+ const g = new THREE.Group(); g.userData.kind = "axes";
331
+ const defs = [
332
+ { name: "build", dir: [1, 0, 0], color: C.green },
333
+ { name: "model", dir: [0, 1, 0], color: C.gold },
334
+ { name: "runtime", dir: [0, 0, 1], color: C.teal },
335
+ ];
336
+ const bars = {};
337
+ defs.forEach((d) => {
338
+ const len = 1.6;
339
+ const bar = new THREE.Mesh(
340
+ new THREE.CylinderGeometry(0.06, 0.06, len, 8),
341
+ new THREE.MeshStandardMaterial({ color: d.color, emissive: d.color, emissiveIntensity: 0.35, transparent: true, opacity: 0.45 }),
342
+ );
343
+ // orient cylinder (default +Y) along dir
344
+ const v = new THREE.Vector3(...d.dir);
345
+ bar.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), v);
346
+ bar.position.copy(v.clone().multiplyScalar(len / 2));
347
+ g.add(bar); bars[d.name] = bar;
348
+ });
349
+ const hub = new THREE.Mesh(
350
+ new THREE.SphereGeometry(0.2, 16, 16),
351
+ new THREE.MeshStandardMaterial({ color: C.accent, emissive: C.accent, emissiveIntensity: 0.4 }),
352
+ );
353
+ g.add(hub);
354
+ g.userData.bars = bars;
355
+ return g;
356
+ }
357
+
358
+ // Demo 6: PQ hybrid-signature node — pq_signing.py (Ed25519 + ML-DSA, FIPS-204
359
+ // ML-DSA-65). Two interlocked rings; hybrid policy = BOTH must verify. ML-DSA is
360
+ // a STRUCTURAL STUB in-sandbox (real:false) until the real PQ signer lands -> gray.
361
+ function buildPqNode(THREE) {
362
+ const g = new THREE.Group(); g.userData.kind = "pq";
363
+ const core = new THREE.Mesh(
364
+ new THREE.IcosahedronGeometry(0.45, 0),
365
+ new THREE.MeshStandardMaterial({ color: C.accent, emissive: C.accent, emissiveIntensity: 0.4, metalness: 0.5, roughness: 0.3 }),
366
+ );
367
+ g.add(core);
368
+ const ed = new THREE.Mesh(
369
+ new THREE.TorusGeometry(0.95, 0.05, 8, 40),
370
+ new THREE.MeshStandardMaterial({ color: C.green, emissive: C.green, emissiveIntensity: 0.45 }), // Ed25519 = real
371
+ );
372
+ g.add(ed);
373
+ const ml = new THREE.Mesh(
374
+ new THREE.TorusGeometry(0.95, 0.05, 8, 40),
375
+ new THREE.MeshStandardMaterial({ color: C.gray, emissive: C.gray, emissiveIntensity: 0.3 }), // ML-DSA = STUB until real
376
+ );
377
+ ml.rotation.x = Math.PI / 2;
378
+ g.add(ml);
379
+ g.userData.core = core; g.userData.ed = ed; g.userData.ml = ml;
380
+ return g;
381
+ }
382
+
383
+ // Demo 7: kill-switch indicator — forge_governance.py kill_switch bool. Armed
384
+ // (true) -> gated actions forced to DENY, throbbing red; safe (false) -> steady green.
385
+ function buildKillSwitch(THREE) {
386
+ const g = new THREE.Group(); g.userData.kind = "killswitch";
387
+ const ring = new THREE.Mesh(
388
+ new THREE.TorusGeometry(0.7, 0.08, 10, 48),
389
+ new THREE.MeshStandardMaterial({ color: C.gray, emissive: C.gray, emissiveIntensity: 0.3 }),
390
+ );
391
+ g.add(ring);
392
+ const core = new THREE.Mesh(
393
+ new THREE.SphereGeometry(0.42, 24, 24),
394
+ new THREE.MeshStandardMaterial({ color: C.green, emissive: C.green, emissiveIntensity: 0.4, metalness: 0.3, roughness: 0.4 }),
395
+ );
396
+ g.add(core);
397
+ g.userData.core = core; g.userData.ring = ring; g.userData.armed = false;
398
+ return g;
399
+ }
400
+
401
+ // Demo 8: signature ≠ safety teaching callout (CVE-2026-45321). A green "valid
402
+ // signature" badge sitting ON a red behavioural-anomaly body — the whole point:
403
+ // a valid signature does NOT make the artifact safe.
404
+ function buildSigSafetyCallout(THREE) {
405
+ const g = new THREE.Group(); g.userData.kind = "callout";
406
+ const body = new THREE.Mesh(
407
+ new THREE.BoxGeometry(1.4, 1.0, 1.0),
408
+ new THREE.MeshStandardMaterial({ color: C.red, emissive: C.red, emissiveIntensity: 0.35, transparent: true, opacity: 0.7 }),
409
+ );
410
+ g.add(body);
411
+ // a green "signature valid" seal stuck on the malicious body
412
+ const seal = new THREE.Mesh(
413
+ new THREE.CylinderGeometry(0.32, 0.32, 0.1, 24),
414
+ new THREE.MeshStandardMaterial({ color: C.green, emissive: C.green, emissiveIntensity: 0.5 }),
415
+ );
416
+ seal.rotation.x = Math.PI / 2; seal.position.set(0, 0, 0.55);
417
+ g.add(seal);
418
+ g.add(_label.billboard(THREE, "STRUCTURAL-ONLY", { text: "signature ≠ safety · CVE-2026-45321", scale: 0.4, position: [0, 1.0, 0] }));
419
+ return g;
420
+ }
421
+
422
+ // ===========================================================================
423
+ // SBOM dependency knowledge graph (GUAC-style) — reuses vendored ForceGraph3D.
424
+ // Demo 9. Renders into its own DOM panel (the UMD lib owns a three.js canvas).
425
+ // Seeded STRUCTURAL; replaced with real nodes/edges when /assurance/artifact 200s.
426
+ // ===========================================================================
427
+ function _seedSbom() {
428
+ // STRUCTURAL placeholder graph (clearly labelled). Real SBOM arrives via the
429
+ // artifact route's dependency closure + behavioural verdict per node.
430
+ const root = { id: "szl/agentic-pinn-solver", group: "root", status: "ROADMAP", val: 8 };
431
+ const nodes = [root];
432
+ const links = [];
433
+ const deps = ["numpy", "fastapi", "starlette", "cryptography", "uvicorn", "pydantic", "ed25519", "rekor-client"];
434
+ deps.forEach((d, i) => {
435
+ const node = { id: d, group: "dep", status: "ROADMAP", val: 3 };
436
+ nodes.push(node);
437
+ links.push({ source: root.id, target: d, rel: "depends-on" });
438
+ if (i % 3 === 0) {
439
+ const sub = { id: d + "·sub", group: "transitive", status: "ROADMAP", val: 2 };
440
+ nodes.push(sub);
441
+ links.push({ source: d, target: sub.id, rel: "depends-on" });
442
+ }
443
+ });
444
+ return { nodes, links };
445
+ }
446
+
447
+ function _initForceGraph() {
448
+ if (!_graphPanel) return;
449
+ const ForceGraph3D = (typeof window !== "undefined") && window.ForceGraph3D;
450
+ if (!ForceGraph3D) {
451
+ // vendored lib not yet on the page — inject it same-origin (0 CDN), then build.
452
+ if (!_fgScript) {
453
+ _fgScript = el("script");
454
+ _fgScript.src = "/vendor/3d-force-graph.min.js";
455
+ _fgScript.onload = () => { try { _buildForceGraph(); } catch (_) {} };
456
+ _fgScript.onerror = () => { if (_hud.sbom) _hud.sbom.textContent = "SBOM graph: vendored lib unavailable"; };
457
+ document.head.appendChild(_fgScript);
458
+ }
459
+ return;
460
+ }
461
+ _buildForceGraph();
462
+ }
463
+
464
+ function _buildForceGraph() {
465
+ const ForceGraph3D = window.ForceGraph3D;
466
+ if (!ForceGraph3D || !_graphPanel || _graph) return;
467
+ const data = _seedSbom();
468
+ _graph = ForceGraph3D()(_graphPanel)
469
+ .backgroundColor("rgba(5,7,13,0)")
470
+ .graphData(data)
471
+ .nodeLabel((n) => `${n.id} · ${n.status}`)
472
+ .nodeColor((n) => "#" + (STATUS_COLOR[n.status] || C.gray).toString(16).padStart(6, "0"))
473
+ .nodeVal((n) => n.val || 3)
474
+ .nodeOpacity(0.92)
475
+ .linkColor(() => "rgba(176,143,255,0.4)")
476
+ .linkDirectionalParticles(1)
477
+ .linkDirectionalParticleSpeed(0.006)
478
+ .width(_graphPanel.clientWidth || 360)
479
+ .height(_graphPanel.clientHeight || 240)
480
+ .showNavInfo(false);
481
+ }
482
+
483
+ // update the SBOM graph from a live /assurance/artifact verdict (real shape).
484
+ function _updateSbomFromArtifact(json) {
485
+ if (!_graph) return;
486
+ // The artifact route returns a behavioural verdict (assess_live_certificate /
487
+ // assess_artifact). When a full dependency closure is present we render it; else
488
+ // we color the single certified artifact node by its verdict.
489
+ const verdict = json.behavioural_verdict || json.verdict;
490
+ const vColor = verdict === "ALLOW" ? "PARTIAL" : (verdict === "DENY" ? null : "ROADMAP");
491
+ const data = _graph.graphData();
492
+ if (data && data.nodes && data.nodes.length) {
493
+ data.nodes.forEach((n) => {
494
+ if (n.group === "root") {
495
+ // DENY -> red; ALLOW -> amber (PARTIAL, never green: signature ≠ safety);
496
+ n.status = vColor || "DENY";
497
+ n._deny = verdict === "DENY";
498
+ }
499
+ });
500
+ _graph.nodeColor((n) => n._deny ? "#ff6b6b" : ("#" + (STATUS_COLOR[n.status] || C.gray).toString(16).padStart(6, "0")));
501
+ _graph.refresh && _graph.refresh();
502
+ }
503
+ }
504
+
505
+ // ===========================================================================
506
+ // OVERLAY HUD — badges per route + legend + teaching callout + SBOM panel
507
+ // ===========================================================================
508
+ function _buildOverlay(ctx) {
509
+ _overlay = el("div", "position:absolute;left:14px;top:14px;z-index:5;display:flex;flex-direction:column;gap:9px;max-width:min(94%,440px);pointer-events:none");
510
+
511
+ const head = el("div", "font:600 14px ui-sans-serif,system-ui;color:#eef3f6;letter-spacing:.4px", TITLE);
512
+ _overlay.appendChild(head);
513
+
514
+ const sub = el("div", "font:10.5px ui-monospace,Menlo,monospace;color:#9fb1bf;line-height:1.5",
515
+ "modeled on GUAC v1.0 · Sigstore/Rekor · SCITT — knowledge graph + Merkle hash-chain + crosswalk");
516
+ _overlay.appendChild(sub);
517
+
518
+ // per-route live badges (each shows NO-LIVE-DATA until Forge meshes it)
519
+ const badgeWrap = el("div", "display:flex;flex-direction:column;gap:5px;pointer-events:auto");
520
+ _hud.badges = {};
521
+ const ROUTE_LABEL = {
522
+ artifact: "artifact (behaviour monitor)", credential: "credential (C2PA)",
523
+ compliance: "compliance (crosswalk)", attest: "attest (build/model/runtime)",
524
+ ledger: "forge ledger (hash-chain)",
525
+ };
526
+ Object.keys(EP).forEach((k) => {
527
+ const row = el("div", "display:flex;align-items:center;gap:8px");
528
+ const tag = el("span", "font:10px ui-monospace,monospace;color:#7d8a96;min-width:182px", ROUTE_LABEL[k]);
529
+ const badge = _live.createBadge();
530
+ _hud.badges[k] = badge;
531
+ row.appendChild(tag); row.appendChild(badge.el);
532
+ badgeWrap.appendChild(row);
533
+ });
534
+ _overlay.appendChild(badgeWrap);
535
+
536
+ // honesty legend (doctrine chips)
537
+ const legend = _label.legend(); legend.style.opacity = "0.9"; legend.style.pointerEvents = "auto";
538
+ _overlay.appendChild(legend);
539
+
540
+ // teaching callout (doctrine): signature ≠ safety
541
+ const teach = el("div",
542
+ "pointer-events:auto;margin-top:2px;padding:8px 10px;border:1px solid #3a2330;border-radius:8px;" +
543
+ "background:rgba(255,107,107,.08);color:#ffb4b4;font:10.5px ui-monospace,Menlo,monospace;line-height:1.55");
544
+ teach.innerHTML =
545
+ "<b style='color:#ff8f8f'>A signature is NOT proof of safety.</b><br>" +
546
+ "CVE-2026-45321 (Mini Shai-Hulud): 84 @tanstack/* npm versions shipped <i>valid</i> " +
547
+ "SLSA L3 / Sigstore provenance. Provenance flagged 0; behaviour caught all 84. " +
548
+ "<span style='color:#9fb1bf'>Λ = Conjecture 1 — advisory governance, NOT proven trust.</span>";
549
+ _overlay.appendChild(teach);
550
+
551
+ // status / scope line (filled live)
552
+ _hud.status = el("div", "font:10px ui-monospace,monospace;color:#7d8a96;line-height:1.5;pointer-events:auto");
553
+ _hud.status.textContent = "awaiting Forge mesh — all 5 assurance routes render NO-LIVE-DATA honestly until 200.";
554
+ _overlay.appendChild(_hud.status);
555
+
556
+ // SBOM graph panel (bottom-right), hosts the GUAC-style force-directed graph
557
+ _graphPanel = el("div",
558
+ "position:absolute;right:14px;bottom:42px;width:min(40vw,380px);height:min(34vh,260px);z-index:5;" +
559
+ "border:1px solid #1b2734;border-radius:10px;background:rgba(7,13,21,.55);overflow:hidden;pointer-events:auto");
560
+ const gh = el("div", "position:absolute;left:8px;top:6px;z-index:2;font:10px ui-monospace,monospace;color:#9fb1bf", "SBOM dependency graph · GUAC-style");
561
+ _hud.sbom = el("div", "position:absolute;left:8px;bottom:6px;z-index:2;font:9.5px ui-monospace,monospace;color:#7d8a96", "STRUCTURAL-ONLY · awaiting /assurance/artifact");
562
+ _graphPanel.appendChild(gh); _graphPanel.appendChild(_hud.sbom);
563
+
564
+ const host = ctx.container || document.body;
565
+ host.appendChild(_overlay);
566
+ host.appendChild(_graphPanel);
567
+ }
568
+
569
+ // ===========================================================================
570
+ // LIVE POLLS — wire each gap route; render honest state; light up on 200.
571
+ // ===========================================================================
572
+ function startPolls(ctx) {
573
+ // 1) artifact behaviour monitor -> SBOM node verdict + status
574
+ _handles.push(_live.poll(EP.artifact, 6000, (json, meta) => {
575
+ _state.artifact = meta;
576
+ _updateSbomFromArtifact(json);
577
+ const v = json.behavioural_verdict || json.verdict;
578
+ if (v && _hud.sbom) {
579
+ const safe = json.signature_alone_is_safety; // doctrine invariant: always false
580
+ _hud.sbom.textContent = `verdict ${v} · sig==safety:${safe === undefined ? "?" : safe} · ${json.fired_monitors ? json.fired_monitors.length : 0} fired`;
581
+ }
582
+ }, { badge: _hud.badges.artifact }));
583
+
584
+ // 2) C2PA credential -> trust hint colors the callout seal honestly
585
+ _handles.push(_live.poll(EP.credential, 7000, (json, meta) => {
586
+ _state.credential = meta;
587
+ // trust_hint ∈ {STRUCTURAL-ONLY, SELF_SIGNED, C2PA_TRUST_LIST, TAMPERED} — never "green"
588
+ const hint = json.trust_hint || (json.active_manifest && json.active_manifest.labels && json.active_manifest.labels.trust);
589
+ const callout = _root && _root.children.find((c) => c.userData && c.userData.kind === "callout");
590
+ if (callout) {
591
+ const seal = callout.children.find((c) => c.geometry && c.geometry.type === "CylinderGeometry");
592
+ if (seal) {
593
+ const col = hint === "TAMPERED" ? C.red : (hint === "C2PA_TRUST_LIST" ? C.green : C.gold);
594
+ seal.material.color.setHex(col); seal.material.emissive.setHex(col);
595
+ }
596
+ }
597
+ }, { badge: _hud.badges.credential }));
598
+
599
+ // 3) compliance crosswalk -> recolor heatmap cells + resize pillars (60/60/0 honest)
600
+ _handles.push(_live.poll(EP.compliance, 8000, (json, meta) => {
601
+ _state.compliance = meta;
602
+ _applyCompliance(json);
603
+ }, { badge: _hud.badges.compliance }));
604
+
605
+ // 4) runtime attestation -> grow the present build/model/runtime axis bars
606
+ _handles.push(_live.poll(EP.attest, 7000, (json, meta) => {
607
+ _state.attest = meta;
608
+ _applyAttest(json);
609
+ }, { badge: _hud.badges.attest }));
610
+
611
+ // 5) forge ledger -> color blocks by ring/decision, drive kill-switch indicator
612
+ _handles.push(_live.poll(EP.ledger, 6000, (json, meta) => {
613
+ _state.ledger = meta;
614
+ _applyLedger(json);
615
+ }, { badge: _hud.badges.ledger }));
616
+
617
+ // roll a compact status line as states change
618
+ _handles.forEach((h) => {});
619
+ _refreshStatus();
620
+ const si = setInterval(_refreshStatus, 2000);
621
+ _handles.push({ stop: () => clearInterval(si) });
622
+ }
623
+
624
+ function _refreshStatus() {
625
+ if (!_hud.status) return;
626
+ const live = Object.keys(_state).filter((k) => _state[k] && _state[k].state === "live");
627
+ const missing = Object.keys(EP).filter((k) => !_state[k] || _state[k].state === "missing" || _state[k].state === "init");
628
+ if (live.length === 0) {
629
+ _hud.status.textContent = `awaiting Forge mesh · ${missing.length}/5 routes NO-LIVE-DATA · viz lights up automatically on 200`;
630
+ _hud.status.style.color = "#7d8a96";
631
+ } else {
632
+ _hud.status.textContent = `LIVE: ${live.join(", ")} · ${missing.length} still awaiting Forge mesh`;
633
+ _hud.status.style.color = "#39d3c4";
634
+ }
635
+ }
636
+
637
+ function _applyCompliance(json) {
638
+ const heatmap = _root && _root.children.find((c) => c.userData && c.userData.kind === "heatmap");
639
+ if (!heatmap) return;
640
+ const cov = (json.coverage || json).frameworks || json.frameworks;
641
+ const crosswalk = json.crosswalk;
642
+ const FW = { NIST_AI_RMF: "NIST", ISO_IEC_42001: "ISO", EU_AI_ACT: "EU" };
643
+ // recolor cells from the real crosswalk[] (control x framework x status)
644
+ if (Array.isArray(crosswalk)) {
645
+ const byFw = { NIST: [], ISO: [], EU: [] };
646
+ crosswalk.forEach((c) => { const f = FW[c.framework]; if (f) byFw[f].push(c.status); });
647
+ heatmap.userData.cells.forEach((cell) => {
648
+ const arr = byFw[cell.userData.framework];
649
+ const st = arr && arr[cell.userData.row];
650
+ if (st && STATUS_COLOR[st]) { cell.material.color.setHex(STATUS_COLOR[st]); cell.material.emissive.setHex(STATUS_COLOR[st]); }
651
+ });
652
+ }
653
+ // resize coverage pillars to real pct_implemented (honest 60/60/0 expected)
654
+ if (cov) {
655
+ Object.entries(FW).forEach(([apiKey, shortKey]) => {
656
+ const entry = cov[apiKey];
657
+ const pil = heatmap.userData.pillars[shortKey];
658
+ if (entry && pil && typeof entry.pct_implemented === "number") {
659
+ const h = Math.max(0.05, (entry.pct_implemented / 100) * 3.5);
660
+ pil.scale.y = h / pil.geometry.parameters.height;
661
+ pil.position.y = h / 2 + 0.2;
662
+ const col = entry.pct_implemented >= 100 ? C.green : (entry.pct_implemented > 0 ? C.gold : C.gray);
663
+ pil.material.color.setHex(col); pil.material.emissive.setHex(col);
664
+ }
665
+ });
666
+ }
667
+ }
668
+
669
+ function _applyAttest(json) {
670
+ const axes = _root && _root.children.find((c) => c.userData && c.userData.kind === "axes");
671
+ if (!axes) return;
672
+ const present = json.axes_present || [];
673
+ ["build", "model", "runtime"].forEach((name) => {
674
+ const bar = axes.userData.bars[name];
675
+ if (!bar) return;
676
+ const on = present.indexOf(name) >= 0;
677
+ bar.material.opacity = on ? 0.95 : 0.3;
678
+ bar.material.emissiveIntensity = on ? 0.55 : 0.2;
679
+ });
680
+ }
681
+
682
+ function _applyLedger(json) {
683
+ const ledger = _root && _root.children.find((c) => c.userData && c.userData.kind === "ledger");
684
+ const killSwitch = _root && _root.children.find((c) => c.userData && c.userData.kind === "killswitch");
685
+ const entries = json.entries || json.replay || [];
686
+ const RING = { "safe-auto": C.green, gated: C.gold, forbidden: C.red, unknown: C.gray };
687
+ if (ledger && Array.isArray(entries)) {
688
+ ledger.userData.blocks.forEach((blk, i) => {
689
+ const e = entries[i];
690
+ if (!e) return;
691
+ if (i === 0) return; // keep genesis blue
692
+ const col = e.decision === "BLOCKED" || e.decision === "DENY" ? C.red : (RING[e.ring] || C.gray);
693
+ blk.material.color.setHex(col); blk.material.emissive.setHex(col);
694
+ });
695
+ }
696
+ // kill-switch: forge_governance kill_switch bool -> armed indicator
697
+ if (killSwitch) {
698
+ const armed = !!json.kill_switch;
699
+ killSwitch.userData.armed = armed;
700
+ const col = armed ? C.red : C.green;
701
+ killSwitch.userData.core.material.color.setHex(col);
702
+ killSwitch.userData.core.material.emissive.setHex(col);
703
+ }
704
+ }
705
+
706
+ // ---------------------------------------------------------------------------
707
+ // unmount
708
+ // ---------------------------------------------------------------------------
709
  function unmount() {
710
+ _handles.forEach((h) => { try { h.stop && h.stop(); } catch (_) {} });
711
+ _handles = [];
712
+ try { if (_frameCb && _stage && _stage.offFrame) _stage.offFrame(_frameCb); } catch (_) {}
713
+ _frameCb = null;
714
+ try { if (_graph && _graph._destructor) _graph._destructor(); } catch (_) {}
715
+ _graph = null;
716
+ try { if (_root && _stage) { disposeObj(_root); _stage.scene.remove(_root); } } catch (_) {}
717
+ _root = null;
718
+ [_overlay, _graphPanel].forEach((n) => { try { if (n && n.parentNode) n.parentNode.removeChild(n); } catch (_) {} });
719
+ _overlay = null; _graphPanel = null; _hud = {};
720
+ _stage = null; _THREE = null; _label = null; _live = null;
721
+ for (const k in _state) delete _state[k];
722
  }
723
 
724
+ export default { id: ID, title: TITLE, endpoints: ENDPOINTS, mount, unmount };