@neuroverseos/nv-sim 0.1.2 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +376 -66
  2. package/dist/adapters/mirofish.js +461 -0
  3. package/dist/adapters/scienceclaw.js +750 -0
  4. package/dist/assets/index-CHmUN8s0.js +532 -0
  5. package/dist/assets/index-DWgMnB7I.css +1 -0
  6. package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
  7. package/dist/assets/reportEngine-BVdQ2_nW.js +1 -0
  8. package/dist/components/ConstraintsPanel.js +11 -0
  9. package/dist/components/StakeholderBuilder.js +32 -0
  10. package/dist/components/ui/badge.js +24 -0
  11. package/dist/components/ui/button.js +70 -0
  12. package/dist/components/ui/card.js +57 -0
  13. package/dist/components/ui/input.js +44 -0
  14. package/dist/components/ui/label.js +45 -0
  15. package/dist/components/ui/select.js +70 -0
  16. package/dist/engine/aiProvider.js +681 -0
  17. package/dist/engine/auditTrace.js +352 -0
  18. package/dist/engine/behavioralAnalysis.js +605 -0
  19. package/dist/engine/cli.js +1408 -299
  20. package/dist/engine/dynamicsGovernance.js +588 -0
  21. package/dist/engine/fullGovernedLoop.js +367 -0
  22. package/dist/engine/governance.js +8 -3
  23. package/dist/engine/governedSimulation.js +114 -17
  24. package/dist/engine/index.js +56 -1
  25. package/dist/engine/liveAdapter.js +342 -0
  26. package/dist/engine/liveVisualizer.js +3063 -0
  27. package/dist/engine/metrics/science.metrics.js +335 -0
  28. package/dist/engine/narrativeInjection.js +305 -0
  29. package/dist/engine/policyEnforcement.js +1611 -0
  30. package/dist/engine/policyEngine.js +799 -0
  31. package/dist/engine/primeRadiant.js +540 -0
  32. package/dist/engine/reasoningEngine.js +57 -3
  33. package/dist/engine/reportEngine.js +97 -0
  34. package/dist/engine/scenarioComparison.js +463 -0
  35. package/dist/engine/scenarioLibrary.js +231 -0
  36. package/dist/engine/swarmSimulation.js +54 -1
  37. package/dist/engine/worldComparison.js +358 -0
  38. package/dist/engine/worldStorage.js +232 -0
  39. package/dist/favicon.ico +0 -0
  40. package/dist/index.html +23 -0
  41. package/dist/lib/reasoningEngine.js +290 -0
  42. package/dist/lib/simulationAdapter.js +686 -0
  43. package/dist/lib/swarmParser.js +291 -0
  44. package/dist/lib/types.js +2 -0
  45. package/dist/lib/utils.js +8 -0
  46. package/dist/placeholder.svg +1 -0
  47. package/dist/robots.txt +14 -0
  48. package/dist/runtime/govern.js +473 -0
  49. package/dist/runtime/index.js +75 -0
  50. package/dist/runtime/types.js +11 -0
  51. package/package.json +17 -12
  52. package/variants/.gitkeep +0 -0
@@ -0,0 +1,3063 @@
1
+ "use strict";
2
+ /**
3
+ * Live Visualizer — Round-by-round simulation viewer
4
+ *
5
+ * Streams simulation data to a local browser dashboard via Server-Sent Events.
6
+ * Watch rules fire, agents react, and governance reshape the system in real time.
7
+ *
8
+ * Usage:
9
+ * nv-sim visualize trading --live
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.startInteractiveServer = startInteractiveServer;
46
+ const http = __importStar(require("http"));
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const swarmSimulation_1 = require("./swarmSimulation");
50
+ const worldBridge_1 = require("./worldBridge");
51
+ const auditTrace_1 = require("./auditTrace");
52
+ const narrativeInjection_1 = require("./narrativeInjection");
53
+ const scenarioLibrary_1 = require("./scenarioLibrary");
54
+ const liveAdapter_1 = require("./liveAdapter");
55
+ function getVariantsDir() {
56
+ return path.resolve(process.cwd(), "variants");
57
+ }
58
+ function ensureVariantsDir() {
59
+ const dir = getVariantsDir();
60
+ if (!fs.existsSync(dir)) {
61
+ fs.mkdirSync(dir, { recursive: true });
62
+ }
63
+ }
64
+ function slugify(name) {
65
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
66
+ }
67
+ function loadAllVariants() {
68
+ ensureVariantsDir();
69
+ const dir = getVariantsDir();
70
+ const files = fs.readdirSync(dir).filter(f => f.endsWith(".json")).sort();
71
+ return files.map(f => {
72
+ try {
73
+ return JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }).filter(Boolean);
79
+ }
80
+ function saveVariant(variant) {
81
+ ensureVariantsDir();
82
+ const filename = `${variant.id}.json`;
83
+ const filepath = path.join(getVariantsDir(), filename);
84
+ fs.writeFileSync(filepath, JSON.stringify(variant, null, 2), "utf-8");
85
+ return filepath;
86
+ }
87
+ function deleteVariant(variantId) {
88
+ const filepath = path.join(getVariantsDir(), `${variantId}.json`);
89
+ if (fs.existsSync(filepath)) {
90
+ fs.unlinkSync(filepath);
91
+ return true;
92
+ }
93
+ return false;
94
+ }
95
+ /**
96
+ * Start an interactive live server with world controls + live simulation.
97
+ *
98
+ * The UI sends POST /run-sim with world parameters.
99
+ * The server runs the simulation and streams results via SSE.
100
+ */
101
+ /**
102
+ * Parse a single plain-English rule into a guard definition.
103
+ * No LLM needed — pattern matching on common financial governance phrases.
104
+ */
105
+ function parseNaturalLanguageRule(line, index) {
106
+ const lower = line.toLowerCase().trim();
107
+ if (!lower)
108
+ return null;
109
+ // Determine enforcement action
110
+ let enforcement = "block";
111
+ if (/^(allow|permit|enable)\b/.test(lower))
112
+ enforcement = "allow";
113
+ else if (/^(pause|review|flag|hold|require.*review|require.*approval)\b/.test(lower))
114
+ enforcement = "pause";
115
+ else if (/^(block|ban|prevent|prohibit|stop|forbid|disallow|no\b|don.t allow)\b/.test(lower))
116
+ enforcement = "block";
117
+ else if (/^limit\b/.test(lower))
118
+ enforcement = "block";
119
+ // Extract intent patterns from the rule text
120
+ const patterns = [];
121
+ const actionWords = {
122
+ "panic sell": ["panic_sell", "panic sell", "panic selling"],
123
+ "panic buy": ["panic_buy", "panic buy", "panic buying"],
124
+ "short sell": ["short", "short_sell", "short selling", "shorting"],
125
+ "leverage": ["increase_leverage", "increase leverage", "max leverage", "excessive leverage"],
126
+ "large trade": ["large_trade", "large trade", "bulk trade", "big trade"],
127
+ "aggressive": ["aggressive_buy", "aggressive buy", "aggressive_short", "aggressive short"],
128
+ "flash": ["flash_trade", "flash trade", "high_frequency"],
129
+ "margin": ["margin_call", "margin call", "increase_margin"],
130
+ "liquidat": ["liquidate", "liquidation", "force_liquidate"],
131
+ "hedge": ["hedge", "hedging", "hedge_position"],
132
+ "buy": ["buy", "aggressive_buy"],
133
+ "sell": ["sell", "selling"],
134
+ "trade": ["trade", "trading"],
135
+ "withdraw": ["withdraw", "withdrawal", "bank_run"],
136
+ "circuit breaker": ["circuit_breaker", "halt_trading", "trading_halt"],
137
+ };
138
+ for (const [keyword, intents] of Object.entries(actionWords)) {
139
+ if (lower.includes(keyword)) {
140
+ patterns.push(...intents);
141
+ }
142
+ }
143
+ // Fallback: extract quoted terms or the main noun phrase
144
+ if (patterns.length === 0) {
145
+ const quoted = line.match(/"([^"]+)"/g);
146
+ if (quoted) {
147
+ quoted.forEach(q => {
148
+ const term = q.replace(/"/g, "").trim();
149
+ patterns.push(term, term.replace(/\s+/g, "_"));
150
+ });
151
+ }
152
+ else {
153
+ // Extract the action part after block/allow/pause
154
+ const actionPart = lower.replace(/^(block|ban|prevent|prohibit|stop|forbid|disallow|allow|permit|enable|pause|review|flag|hold|limit|no|don.t allow)\s+/i, "").trim();
155
+ if (actionPart) {
156
+ patterns.push(actionPart, actionPart.replace(/\s+/g, "_"));
157
+ }
158
+ }
159
+ }
160
+ // Deduplicate
161
+ const uniquePatterns = [...new Set(patterns)];
162
+ if (uniquePatterns.length === 0)
163
+ return null;
164
+ return {
165
+ id: `custom-rule-${index}`,
166
+ description: line,
167
+ enforcement,
168
+ intent_patterns: uniquePatterns,
169
+ category: "custom",
170
+ };
171
+ }
172
+ function startInteractiveServer(port, onReady) {
173
+ const clients = new Set();
174
+ let isRunning = false;
175
+ // ── Persistent Audit Trail ──
176
+ // Every governance verdict is written to disk as JSONL
177
+ const auditTrail = new auditTrace_1.AuditTrail();
178
+ // Custom guards from plain-English rule editor (session-scoped)
179
+ const customGuards = [];
180
+ let currentSession = {
181
+ id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
182
+ startedAt: new Date().toISOString(),
183
+ world: "trading",
184
+ guardCount: 0,
185
+ evaluations: [],
186
+ };
187
+ // Session snapshots for multi-run comparison
188
+ const sessionHistory = [];
189
+ function synthesizeSessionReport() {
190
+ // Gather all sessions to report on (history + current if it has data)
191
+ const allSessions = [
192
+ ...sessionHistory,
193
+ ...(currentSession.evaluations.length > 0 ? [currentSession] : []),
194
+ ];
195
+ if (allSessions.length === 0) {
196
+ return {
197
+ sessionId: currentSession.id,
198
+ scenario: "Live governance session (no evaluations yet)",
199
+ runs: [],
200
+ divergence: {
201
+ stabilityTrend: [],
202
+ collapseTrend: [],
203
+ effectivenessTrend: [],
204
+ bestIteration: 0,
205
+ worstIteration: 0,
206
+ totalDivergence: 0,
207
+ narrative: "No evaluations recorded. Send actions to POST /api/evaluate to begin.",
208
+ },
209
+ recommendation: "Start sending agent actions to /api/evaluate to generate governance data.",
210
+ generatedAt: new Date().toISOString(),
211
+ };
212
+ }
213
+ const runs = allSessions.map((sess, idx) => {
214
+ const evals = sess.evaluations;
215
+ const total = evals.length;
216
+ const blocked = evals.filter(e => e.decision === "BLOCK").length;
217
+ const modified = evals.filter(e => e.decision === "MODIFY").length;
218
+ const allowed = evals.filter(e => e.decision === "ALLOW").length;
219
+ // Synthesize metrics from real evaluation data
220
+ const blockRate = total > 0 ? blocked / total : 0;
221
+ const interventionRate = total > 0 ? (blocked + modified) / total : 0;
222
+ // Stability: higher when governance is actively catching harmful actions
223
+ // If nothing is blocked, either the system is clean OR governance is too weak
224
+ const stabilityScore = total > 0
225
+ ? Math.min(0.95, 0.4 + interventionRate * 0.4 + (allowed / Math.max(1, total)) * 0.2)
226
+ : 0.5;
227
+ // Collapse probability: lower when more harmful actions are caught
228
+ const collapseProbability = total > 0
229
+ ? Math.max(0.02, 0.6 - interventionRate * 0.5 - blockRate * 0.15)
230
+ : 0.5;
231
+ // Governance effectiveness: composite of intervention quality
232
+ const governanceEffectiveness = total > 0
233
+ ? Math.min(0.95, interventionRate * 0.6 + blockRate * 0.3 + (total > 10 ? 0.1 : 0))
234
+ : 0;
235
+ // Unique rules that fired
236
+ const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
237
+ // Unique actors and actions
238
+ const uniqueActors = [...new Set(evals.map(e => e.actor))];
239
+ const uniqueActions = [...new Set(evals.map(e => e.action))];
240
+ return {
241
+ iteration: idx + 1,
242
+ worldName: sess.world || "live-session",
243
+ ruleCount: sess.guardCount,
244
+ gateCount: 0,
245
+ metrics: {
246
+ avgImpact: interventionRate > 0 ? -(interventionRate * 0.5) : 0.1,
247
+ collapseProbability,
248
+ stabilityScore,
249
+ coalitionRisks: 0,
250
+ polarizationEvents: 0,
251
+ peakNegativeSentiment: blockRate > 0.3 ? -0.6 : -0.2,
252
+ consensusRounds: 0,
253
+ maxVolatility: blockRate > 0.2 ? 0.7 : 0.3,
254
+ },
255
+ comparison: {
256
+ collapseReduction: collapseProbability < 0.5 ? (0.6 - collapseProbability) : 0,
257
+ stabilityImprovement: stabilityScore > 0.5 ? (stabilityScore - 0.4) : 0,
258
+ volatilityReduction: interventionRate * 0.5,
259
+ coalitionRiskReduction: 0,
260
+ governanceEffectiveness,
261
+ narrative: total > 0
262
+ ? `${total} actions evaluated: ${blocked} blocked, ${modified} modified, ${allowed} allowed across ${uniqueActors.length} agent(s) performing ${uniqueActions.length} action type(s).`
263
+ : "No evaluations in this session.",
264
+ },
265
+ governanceStats: {
266
+ engineLoaded: true,
267
+ totalEvaluations: total,
268
+ verdicts: { allow: allowed, block: blocked, pause: modified },
269
+ rulesFired: triggeredRules.length,
270
+ worldCollapsed: false,
271
+ finalViability: stabilityScore > 0.6 ? "stable" : stabilityScore > 0.4 ? "at-risk" : "critical",
272
+ invariantsChecked: sess.guardCount,
273
+ triggeredGuards: triggeredRules,
274
+ },
275
+ };
276
+ });
277
+ // Compute rule changes between sessions
278
+ for (let i = 1; i < runs.length; i++) {
279
+ const prev = allSessions[i - 1];
280
+ const curr = allSessions[i];
281
+ if (prev.guardCount !== curr.guardCount) {
282
+ runs[i].ruleChanges = {
283
+ added: [],
284
+ removed: [],
285
+ gatesAdded: [],
286
+ gatesRemoved: [],
287
+ thesisChanged: prev.world !== curr.world,
288
+ };
289
+ }
290
+ }
291
+ // Divergence analysis
292
+ const stabilityTrend = runs.map(r => r.metrics.stabilityScore);
293
+ const collapseTrend = runs.map(r => r.metrics.collapseProbability);
294
+ const effectivenessTrend = runs.map(r => r.comparison.governanceEffectiveness);
295
+ let bestIteration = 1;
296
+ let worstIteration = 1;
297
+ let bestScore = -Infinity;
298
+ let worstScore = Infinity;
299
+ for (const run of runs) {
300
+ if (run.comparison.governanceEffectiveness > bestScore) {
301
+ bestScore = run.comparison.governanceEffectiveness;
302
+ bestIteration = run.iteration;
303
+ }
304
+ if (run.comparison.governanceEffectiveness < worstScore) {
305
+ worstScore = run.comparison.governanceEffectiveness;
306
+ worstIteration = run.iteration;
307
+ }
308
+ }
309
+ let totalDivergence = 0;
310
+ for (let i = 1; i < runs.length; i++) {
311
+ totalDivergence += Math.abs(runs[i].metrics.stabilityScore - runs[i - 1].metrics.stabilityScore);
312
+ totalDivergence += Math.abs(runs[i].metrics.collapseProbability - runs[i - 1].metrics.collapseProbability);
313
+ totalDivergence += Math.abs(runs[i].comparison.governanceEffectiveness - runs[i - 1].comparison.governanceEffectiveness);
314
+ }
315
+ // Build narrative
316
+ const totalEvals = allSessions.reduce((sum, s) => sum + s.evaluations.length, 0);
317
+ const totalBlocked = allSessions.reduce((sum, s) => sum + s.evaluations.filter(e => e.decision === "BLOCK").length, 0);
318
+ const narrativeParts = [];
319
+ narrativeParts.push(`${allSessions.length} session(s), ${totalEvals} total evaluations, ${totalBlocked} blocked.`);
320
+ if (runs.length > 1) {
321
+ const stabDelta = stabilityTrend[stabilityTrend.length - 1] - stabilityTrend[0];
322
+ if (stabDelta > 0.05)
323
+ narrativeParts.push(`Stability improved ${(stabDelta * 100).toFixed(0)}pp across sessions.`);
324
+ narrativeParts.push(`Best outcome: session ${bestIteration}.`);
325
+ }
326
+ const best = runs[bestIteration - 1];
327
+ const recommendation = runs.length === 1
328
+ ? `Single session: ${best.governanceStats.totalEvaluations} evaluations, ${best.governanceStats.verdicts.block} blocked. Apply different rules and reset to compare.`
329
+ : `Best session: #${bestIteration} ("${best.worldName}") — ${(best.comparison.governanceEffectiveness * 100).toFixed(0)}% effectiveness, ${(best.metrics.stabilityScore * 100).toFixed(0)}% stability.`;
330
+ return {
331
+ sessionId: currentSession.id,
332
+ scenario: `Live governance — ${currentSession.world}`,
333
+ runs,
334
+ divergence: {
335
+ stabilityTrend,
336
+ collapseTrend,
337
+ effectivenessTrend,
338
+ bestIteration,
339
+ worstIteration,
340
+ totalDivergence: Number(totalDivergence.toFixed(3)),
341
+ narrative: narrativeParts.join(" "),
342
+ },
343
+ recommendation,
344
+ generatedAt: new Date().toISOString(),
345
+ };
346
+ }
347
+ function broadcast(event) {
348
+ const data = `data: ${JSON.stringify(event)}\n\n`;
349
+ for (const client of clients) {
350
+ client.write(data);
351
+ }
352
+ }
353
+ function readBody(req) {
354
+ return new Promise((resolve) => {
355
+ let body = "";
356
+ req.on("data", (chunk) => { body += chunk.toString(); });
357
+ req.on("end", () => resolve(body));
358
+ });
359
+ }
360
+ function jsonResponse(res, status, data) {
361
+ res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
362
+ res.end(JSON.stringify(data));
363
+ }
364
+ async function runSimulation(config) {
365
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
366
+ const { runGovernedComparison } = await Promise.resolve().then(() => __importStar(require("./governedSimulation")));
367
+ const resolved = resolveWorld(config.worldId);
368
+ const rounds = config.rounds ?? resolved.swarm.rounds ?? 5;
369
+ // Apply state variable overrides to world definition
370
+ const world = { ...resolved.world };
371
+ if (config.stateOverrides) {
372
+ const updatedVars = world.state_variables.map(sv => {
373
+ if (config.stateOverrides && sv.id in config.stateOverrides) {
374
+ return { ...sv, default_value: config.stateOverrides[sv.id] };
375
+ }
376
+ return sv;
377
+ });
378
+ world.state_variables = updatedVars;
379
+ }
380
+ // Inject custom rules as first-class invariants (not just guards)
381
+ // This ensures user rules affect BOTH governance paths:
382
+ // 1. Guard engine (intent-level blocking) — already wired via customGuards[]
383
+ // 2. Invariant engine (system-level stability shaping) — wired here
384
+ if (customGuards.length > 0) {
385
+ const customInvariants = customGuards.map(cg => ({
386
+ id: cg.id,
387
+ description: cg.description,
388
+ enforceable: cg.enforcement === "block",
389
+ }));
390
+ world.invariants = [...world.invariants, ...customInvariants];
391
+ }
392
+ // Resolve narrative events
393
+ let narrativeEvents = [];
394
+ if (config.scenarioId && scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]) {
395
+ narrativeEvents = (0, scenarioLibrary_1.resolveScenarioEvents)(scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]);
396
+ }
397
+ else if (config.injectEvents && config.injectEvents.length > 0) {
398
+ narrativeEvents = (0, narrativeInjection_1.parseInjectArgs)(["--inject", config.injectEvents.join(",")]);
399
+ }
400
+ const request = {
401
+ scenario: resolved.scenario,
402
+ stakeholders: resolved.stakeholders,
403
+ assumptions: resolved.assumptions,
404
+ constraints: resolved.constraints,
405
+ depth: resolved.depth,
406
+ swarm: { ...resolved.swarm, rounds },
407
+ };
408
+ // Send meta event
409
+ broadcast({
410
+ type: "meta",
411
+ source: "nv-sim",
412
+ scenario: resolved.scenario,
413
+ worldThesis: world.thesis,
414
+ agents: resolved.stakeholders.map(s => s.id),
415
+ invariants: world.invariants.map(inv => ({
416
+ id: inv.id,
417
+ description: inv.description,
418
+ enforcement: inv.enforceable ? "full" : "advisory",
419
+ source: inv.id.startsWith("custom-rule-") ? "user" : "world",
420
+ })),
421
+ gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
422
+ narrativeEvents: narrativeEvents.map(e => ({ id: e.id, headline: e.headline, round: e.round, severity: e.severity })),
423
+ totalRounds: rounds,
424
+ });
425
+ // Run baseline
426
+ const baselineResult = await (0, swarmSimulation_1.runSwarmSimulation)(resolved.scenario, resolved.stakeholders, resolved.paths, { ...resolved.swarm, rounds });
427
+ for (const round of baselineResult.rounds) {
428
+ broadcast({
429
+ type: "round",
430
+ round: round.round,
431
+ totalRounds: rounds,
432
+ phase: "baseline",
433
+ reactions: round.reactions.map(r => ({
434
+ stakeholder_id: r.stakeholder_id,
435
+ reaction: r.reaction,
436
+ impact: r.impact,
437
+ confidence: r.confidence,
438
+ trigger: r.trigger,
439
+ })),
440
+ avgImpact: round.reactions.reduce((s, r) => s + r.impact, 0) / round.reactions.length,
441
+ maxVolatility: Math.max(...round.reactions.map(r => Math.abs(r.impact))),
442
+ dynamics: round.emergent_dynamics ?? [],
443
+ interventionCount: 0,
444
+ });
445
+ await new Promise(r => setTimeout(r, 400));
446
+ }
447
+ // Run governed with narrative events
448
+ const governedResult = await runGovernedComparison(request, world, resolved.paths, narrativeEvents);
449
+ const nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, world);
450
+ for (const round of governedResult.governed.swarm.rounds) {
451
+ const reactions = round.reactions.map(r => {
452
+ const verdict = (0, worldBridge_1.evaluateScenarioGuard)({ ...request, scenario: `[R${round.round}] ${r.stakeholder_id}: ${r.reaction}` }, nvWorld, { trace: true, level: "standard" });
453
+ return {
454
+ stakeholder_id: r.stakeholder_id,
455
+ reaction: r.reaction,
456
+ impact: r.impact,
457
+ confidence: r.confidence,
458
+ trigger: r.trigger,
459
+ verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
460
+ };
461
+ });
462
+ const interventionCount = (round.emergent_dynamics ?? []).filter(d => d.includes("intervention") || d.includes("governance")).length;
463
+ broadcast({
464
+ type: "round",
465
+ round: round.round,
466
+ totalRounds: rounds,
467
+ phase: "governed",
468
+ reactions,
469
+ avgImpact: round.reactions.reduce((s, r) => s + r.impact, 0) / round.reactions.length,
470
+ maxVolatility: Math.max(...round.reactions.map(r => Math.abs(r.impact))),
471
+ dynamics: round.emergent_dynamics ?? [],
472
+ interventionCount,
473
+ });
474
+ await new Promise(r => setTimeout(r, 500));
475
+ }
476
+ broadcast({ type: "complete", result: governedResult });
477
+ }
478
+ const server = http.createServer(async (req, res) => {
479
+ // CORS preflight
480
+ if (req.method === "OPTIONS") {
481
+ res.writeHead(204, {
482
+ "Access-Control-Allow-Origin": "*",
483
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
484
+ "Access-Control-Allow-Headers": "Content-Type",
485
+ });
486
+ res.end();
487
+ return;
488
+ }
489
+ if (req.url === "/events") {
490
+ res.writeHead(200, {
491
+ "Content-Type": "text/event-stream",
492
+ "Cache-Control": "no-cache",
493
+ "Connection": "keep-alive",
494
+ "Access-Control-Allow-Origin": "*",
495
+ });
496
+ clients.add(res);
497
+ req.on("close", () => clients.delete(res));
498
+ return;
499
+ }
500
+ if (req.url === "/api/worlds" && req.method === "GET") {
501
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
502
+ const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis"];
503
+ const worlds = worldIds.map(id => {
504
+ try {
505
+ const r = resolveWorld(id);
506
+ return {
507
+ id,
508
+ title: r.title,
509
+ thesis: r.world.thesis,
510
+ stateVariables: r.world.state_variables,
511
+ invariants: r.world.invariants,
512
+ gates: r.world.gates ?? [],
513
+ };
514
+ }
515
+ catch {
516
+ return null;
517
+ }
518
+ }).filter(Boolean);
519
+ jsonResponse(res, 200, { worlds });
520
+ return;
521
+ }
522
+ if (req.url === "/api/scenarios" && req.method === "GET") {
523
+ jsonResponse(res, 200, { scenarios: scenarioLibrary_1.SCENARIO_LIBRARY });
524
+ return;
525
+ }
526
+ if (req.url === "/api/narratives" && req.method === "GET") {
527
+ jsonResponse(res, 200, { narratives: narrativeInjection_1.NARRATIVE_PRESETS });
528
+ return;
529
+ }
530
+ if (req.url === "/api/variants" && req.method === "GET") {
531
+ const variants = loadAllVariants();
532
+ jsonResponse(res, 200, { variants });
533
+ return;
534
+ }
535
+ if (req.url === "/api/save-variant" && req.method === "POST") {
536
+ try {
537
+ const body = await readBody(req);
538
+ const payload = JSON.parse(body);
539
+ if (!payload.name || !payload.baseWorld) {
540
+ jsonResponse(res, 400, { error: "name and baseWorld are required" });
541
+ return;
542
+ }
543
+ const id = slugify(payload.name);
544
+ const variant = {
545
+ id,
546
+ name: payload.name,
547
+ description: payload.description ?? "",
548
+ baseWorld: payload.baseWorld,
549
+ stateOverrides: payload.stateOverrides ?? {},
550
+ events: payload.events ?? [],
551
+ rounds: payload.rounds ?? 5,
552
+ createdAt: new Date().toISOString(),
553
+ lastResult: payload.lastResult,
554
+ };
555
+ const filepath = saveVariant(variant);
556
+ jsonResponse(res, 200, { status: "saved", variant, filepath });
557
+ }
558
+ catch (err) {
559
+ jsonResponse(res, 400, { error: "Invalid request body" });
560
+ }
561
+ return;
562
+ }
563
+ if (req.url?.startsWith("/api/delete-variant/") && req.method === "DELETE") {
564
+ const variantId = req.url.split("/api/delete-variant/")[1];
565
+ if (variantId && deleteVariant(variantId)) {
566
+ jsonResponse(res, 200, { status: "deleted", id: variantId });
567
+ }
568
+ else {
569
+ jsonResponse(res, 404, { error: "Variant not found" });
570
+ }
571
+ return;
572
+ }
573
+ if (req.url === "/api/run-sim" && req.method === "POST") {
574
+ if (isRunning) {
575
+ jsonResponse(res, 409, { error: "Simulation already running" });
576
+ return;
577
+ }
578
+ try {
579
+ const body = await readBody(req);
580
+ const config = JSON.parse(body);
581
+ isRunning = true;
582
+ jsonResponse(res, 200, { status: "started", config });
583
+ // Run simulation async, streaming via SSE
584
+ runSimulation(config).catch(err => {
585
+ broadcast({ type: "complete", result: { error: String(err) } });
586
+ }).finally(() => { isRunning = false; });
587
+ }
588
+ catch (err) {
589
+ jsonResponse(res, 400, { error: "Invalid request body" });
590
+ }
591
+ return;
592
+ }
593
+ // ── Governance Evaluate Endpoint ──
594
+ // Universal bridge endpoint: external simulators POST actions here for governance evaluation.
595
+ // Contract: { actor, action, payload, state? } → { decision: ALLOW|BLOCK|MODIFY, reason, modified_action? }
596
+ if (req.url === "/api/evaluate" && req.method === "POST") {
597
+ try {
598
+ const body = await readBody(req);
599
+ const payload = JSON.parse(body);
600
+ if (!payload.actor || !payload.action) {
601
+ jsonResponse(res, 400, { error: "actor and action are required" });
602
+ return;
603
+ }
604
+ // Resolve world for evaluation
605
+ const worldId = payload.world ?? "trading";
606
+ let nvWorld;
607
+ try {
608
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
609
+ const resolved = resolveWorld(worldId);
610
+ const request = {
611
+ scenario: resolved.scenario,
612
+ stakeholders: resolved.stakeholders,
613
+ assumptions: resolved.assumptions,
614
+ constraints: resolved.constraints,
615
+ depth: resolved.depth,
616
+ swarm: resolved.swarm,
617
+ };
618
+ nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, resolved.world);
619
+ }
620
+ catch {
621
+ // Fallback: build minimal world
622
+ nvWorld = (0, worldBridge_1.buildWorldFromScenario)({
623
+ scenario: `${payload.actor}: ${payload.action}`,
624
+ stakeholders: [],
625
+ });
626
+ }
627
+ // Enable action_space — bridge actions are simulation execution, not thinking-only.
628
+ // Without this, the safety layer flags normal actions like "buy" as execution intents
629
+ // in a thinking-only environment.
630
+ nvWorld.world.players = { ...nvWorld.world.players, action_space: true };
631
+ // Inject simulation-specific guards into the world.
632
+ // The guard engine pattern-matches event.intent against guard.intent_patterns.
633
+ // Without guards, everything defaults to ALLOW. These guards define what
634
+ // simulation actions should be governed.
635
+ if (!nvWorld.guards) {
636
+ // Detect world type from request — social media worlds get social guards,
637
+ // financial worlds get trading guards, unknown gets both.
638
+ const isSocial = payload.world === "social-media" || payload.world === "social"
639
+ || ["create_post", "like_post", "repost", "follow", "unfollow", "create_comment",
640
+ "search_posts", "mute", "unmute", "trend"].includes(payload.action);
641
+ const financialGuards = [
642
+ {
643
+ id: "sim-panic-actions",
644
+ label: "Block panic-driven actions",
645
+ description: "Prevents panic selling, aggressive shorting, and other destabilizing actions during high volatility",
646
+ category: "structural",
647
+ enforcement: "block",
648
+ immutable: true,
649
+ invariant_ref: nvWorld.invariants[0]?.id,
650
+ intent_patterns: ["panic_sell", "panic sell", "panic buy", "panic_buy"],
651
+ default_enabled: true,
652
+ },
653
+ {
654
+ id: "sim-excessive-leverage",
655
+ label: "Block excessive leverage",
656
+ description: "Prevents increasing leverage positions that could amplify cascades",
657
+ category: "structural",
658
+ enforcement: "block",
659
+ immutable: true,
660
+ intent_patterns: ["increase_leverage", "increase leverage", "max leverage"],
661
+ default_enabled: true,
662
+ },
663
+ {
664
+ id: "sim-aggressive-actions",
665
+ label: "Pause aggressive market actions",
666
+ description: "Requires review for aggressive buying or shorting that could move markets",
667
+ category: "operational",
668
+ enforcement: "pause",
669
+ immutable: false,
670
+ intent_patterns: ["aggressive_buy", "aggressive buy", "aggressive_short", "short"],
671
+ default_enabled: true,
672
+ },
673
+ ];
674
+ // Social media guards for MiroFish/OASIS agent actions.
675
+ // These govern what AI agents can do on simulated social platforms.
676
+ const socialGuards = [
677
+ {
678
+ id: "social-harmful-content",
679
+ label: "Block harmful content creation",
680
+ description: "Prevents agents from posting content that incites panic, spreads disinformation, or promotes harmful behavior",
681
+ category: "structural",
682
+ enforcement: "block",
683
+ immutable: true,
684
+ intent_patterns: ["create_post", "create_comment", "quote_post"],
685
+ content_patterns: ["panic", "crash", "collaps", "sell everything", "market is dead", "scam", "rug pull", "ponzi"],
686
+ default_enabled: true,
687
+ },
688
+ {
689
+ id: "social-coordinated-manipulation",
690
+ label: "Block coordinated manipulation",
691
+ description: "Prevents agents from engaging in coordinated inauthentic behavior like mass following, mass liking, or brigading",
692
+ category: "structural",
693
+ enforcement: "block",
694
+ immutable: true,
695
+ intent_patterns: ["follow", "like_post", "repost"],
696
+ default_enabled: true,
697
+ },
698
+ {
699
+ id: "social-spam-prevention",
700
+ label: "Pause high-frequency posting",
701
+ description: "Rate-limits agents that post too frequently, preventing spam and platform flooding",
702
+ category: "operational",
703
+ enforcement: "pause",
704
+ immutable: false,
705
+ intent_patterns: ["create_post", "create_comment", "repost", "quote_post"],
706
+ default_enabled: true,
707
+ },
708
+ {
709
+ id: "social-engagement-farming",
710
+ label: "Block engagement farming",
711
+ description: "Prevents agents from like-bombing, follow-unfollowing, or other engagement manipulation tactics",
712
+ category: "operational",
713
+ enforcement: "block",
714
+ immutable: false,
715
+ intent_patterns: ["like_post", "unlike_post", "follow", "unfollow"],
716
+ default_enabled: true,
717
+ },
718
+ ];
719
+ const guards = isSocial ? socialGuards : financialGuards;
720
+ // Build intent vocabulary from selected guards
721
+ const intentVocabulary = {};
722
+ for (const g of guards) {
723
+ for (const pat of g.intent_patterns) {
724
+ if (!intentVocabulary[pat]) {
725
+ intentVocabulary[pat] = { label: pat.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()), pattern: pat };
726
+ }
727
+ }
728
+ }
729
+ nvWorld.guards = { guards, intent_vocabulary: intentVocabulary };
730
+ }
731
+ // Inject custom guards from plain-English rule editor
732
+ if (customGuards.length > 0 && nvWorld.guards) {
733
+ const existingGuards = nvWorld.guards.guards;
734
+ for (const cg of customGuards) {
735
+ existingGuards.push({
736
+ id: cg.id,
737
+ label: cg.label,
738
+ description: cg.description,
739
+ category: cg.category,
740
+ enforcement: cg.enforcement,
741
+ immutable: false,
742
+ intent_patterns: cg.intent_patterns,
743
+ default_enabled: true,
744
+ });
745
+ // Add patterns to vocabulary
746
+ for (const pat of cg.intent_patterns) {
747
+ nvWorld.guards.intent_vocabulary[pat] = { label: pat, pattern: pat };
748
+ }
749
+ }
750
+ }
751
+ // Content-aware governance for social media actions.
752
+ // The guard engine matches intent against intent_patterns, but for social media
753
+ // we also need to check message content for harmful patterns.
754
+ const messageContent = typeof payload.payload?.message === "string"
755
+ ? payload.payload.message.toLowerCase()
756
+ : typeof payload.payload?.content === "string"
757
+ ? payload.payload.content.toLowerCase()
758
+ : "";
759
+ const contentPatterns = ["panic", "crash", "collaps", "sell everything", "market is dead",
760
+ "scam", "rug pull", "ponzi", "buy now or lose", "guaranteed returns", "get rich"];
761
+ const hasHarmfulContent = messageContent.length > 0
762
+ && contentPatterns.some(p => messageContent.includes(p));
763
+ // If content is harmful and action is content-creating, short-circuit to BLOCK
764
+ const isContentAction = ["create_post", "create_comment", "quote_post", "repost"].includes(payload.action);
765
+ if (hasHarmfulContent && isContentAction) {
766
+ const matchedPattern = contentPatterns.find(p => messageContent.includes(p)) ?? "harmful content";
767
+ broadcast({
768
+ type: "round",
769
+ round: 0,
770
+ totalRounds: 0,
771
+ phase: "governed",
772
+ reactions: [{
773
+ stakeholder_id: payload.actor,
774
+ reaction: payload.action,
775
+ impact: 0,
776
+ confidence: 0.9,
777
+ trigger: "bridge",
778
+ verdict: { status: "BLOCK", reason: `Content violates governance: "${matchedPattern}" detected`, ruleId: "social-harmful-content" },
779
+ }],
780
+ avgImpact: 0,
781
+ maxVolatility: 0,
782
+ dynamics: [],
783
+ interventionCount: 1,
784
+ });
785
+ // Record in session
786
+ currentSession.evaluations.push({
787
+ actor: payload.actor, action: payload.action, decision: "BLOCK",
788
+ reason: `Content blocked: "${matchedPattern}" detected`,
789
+ ruleId: "social-harmful-content", world: payload.world ?? currentSession.world,
790
+ timestamp: Date.now(), payload: payload.payload,
791
+ });
792
+ jsonResponse(res, 200, {
793
+ decision: "BLOCK",
794
+ reason: `Content blocked: "${matchedPattern}" detected in ${payload.action} — violates social media governance policy`,
795
+ rule_id: "social-harmful-content",
796
+ evidence: { matched_pattern: matchedPattern, action: payload.action, actor: payload.actor },
797
+ modified_action: null,
798
+ });
799
+ return;
800
+ }
801
+ // Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
802
+ // Omit `direction` — setting it enables execution-intent safety checks (prompt injection
803
+ // detection) which falsely flag financial terms like "buy" and "sell". Bridge actions are
804
+ // simulation commands, not user prompts.
805
+ const guardEvent = {
806
+ intent: payload.action,
807
+ tool: "simulation",
808
+ scope: `bridge/${payload.actor}`,
809
+ actionCategory: "execute",
810
+ riskLevel: (["panic_sell", "panic_buy", "increase_leverage", "create_post", "repost", "quote_post"].includes(payload.action) ? "high" : "medium"),
811
+ args: {
812
+ actor: payload.actor,
813
+ action: payload.action,
814
+ ...(payload.payload ?? {}),
815
+ },
816
+ };
817
+ // Evaluate directly via the governance module
818
+ let verdict;
819
+ try {
820
+ const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
821
+ verdict = nv.evaluateGuard(guardEvent, nvWorld, {
822
+ trace: true,
823
+ level: "standard",
824
+ });
825
+ }
826
+ catch {
827
+ // Fallback to scenario guard evaluation
828
+ verdict = (0, worldBridge_1.evaluateScenarioGuard)({
829
+ scenario: `[BRIDGE] ${payload.actor}: ${payload.action}`,
830
+ stakeholders: [{ id: payload.actor, description: payload.actor, disposition: "neutral", priorities: [] }],
831
+ }, nvWorld, { trace: true, level: "standard" });
832
+ }
833
+ // Map verdict to bridge protocol
834
+ const decision = verdict.status === "BLOCK" ? "BLOCK"
835
+ : verdict.status === "PAUSE" ? "MODIFY"
836
+ : "ALLOW";
837
+ // Broadcast governance event to connected SSE clients
838
+ broadcast({
839
+ type: "round",
840
+ round: 0,
841
+ totalRounds: 0,
842
+ phase: "governed",
843
+ reactions: [{
844
+ stakeholder_id: payload.actor,
845
+ reaction: payload.action,
846
+ impact: 0,
847
+ confidence: 0.5,
848
+ trigger: "bridge",
849
+ verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
850
+ }],
851
+ avgImpact: 0,
852
+ maxVolatility: 0,
853
+ dynamics: [],
854
+ interventionCount: decision !== "ALLOW" ? 1 : 0,
855
+ });
856
+ // Record in session
857
+ currentSession.evaluations.push({
858
+ actor: payload.actor, action: payload.action,
859
+ decision: decision,
860
+ reason: verdict.reason ?? "", ruleId: verdict.ruleId ?? null,
861
+ world: payload.world ?? currentSession.world,
862
+ timestamp: Date.now(), payload: payload.payload,
863
+ });
864
+ currentSession.guardCount = customGuards.length + (nvWorld.guards?.guards?.length ?? 0);
865
+ // Persist to audit trail on disk
866
+ auditTrail.logVerdict({
867
+ agent: payload.actor,
868
+ action: payload.action,
869
+ actionType: payload.payload?.type ?? "unknown",
870
+ verdict: decision,
871
+ reason: verdict.reason ?? "",
872
+ confidence: verdict.confidence ?? 0.5,
873
+ rulesFired: verdict.ruleId ? [{
874
+ id: verdict.ruleId,
875
+ description: verdict.reason ?? "",
876
+ effect: decision === "BLOCK" ? "blocked" : decision === "MODIFY" ? "dampened" : "monitored",
877
+ impactReduction: decision === "BLOCK" ? 1 : decision === "MODIFY" ? 0.5 : 0,
878
+ }] : [],
879
+ worldState: payload.world ?? currentSession.world,
880
+ });
881
+ jsonResponse(res, 200, {
882
+ decision,
883
+ reason: verdict.reason ?? null,
884
+ rule_id: verdict.ruleId ?? null,
885
+ evidence: verdict.evidence ?? null,
886
+ modified_action: decision === "MODIFY" ? payload.payload : null,
887
+ });
888
+ }
889
+ catch (err) {
890
+ // Record fail-open in session (payload may be out of scope if JSON parse failed)
891
+ currentSession.evaluations.push({
892
+ actor: "unknown", action: "unknown",
893
+ decision: "ALLOW", reason: "Governance evaluation error — fail open",
894
+ ruleId: null, world: currentSession.world, timestamp: Date.now(),
895
+ });
896
+ // Fail open — return ALLOW on any error
897
+ jsonResponse(res, 200, {
898
+ decision: "ALLOW",
899
+ reason: "Governance evaluation error — fail open",
900
+ rule_id: null,
901
+ evidence: null,
902
+ modified_action: null,
903
+ });
904
+ }
905
+ return;
906
+ }
907
+ // ── Plain-English Rule Parser ──
908
+ // Parses natural language rules into guard definitions (local, no LLM needed)
909
+ if (req.url === "/api/parse-rules" && req.method === "POST") {
910
+ try {
911
+ const body = await readBody(req);
912
+ const payload = JSON.parse(body);
913
+ if (!payload.text) {
914
+ jsonResponse(res, 400, { error: "text is required" });
915
+ return;
916
+ }
917
+ const lines = payload.text.split("\n").map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith("#"));
918
+ const rules = lines.map((line, i) => parseNaturalLanguageRule(line, i));
919
+ jsonResponse(res, 200, { rules: rules.filter(Boolean), raw: lines });
920
+ }
921
+ catch (err) {
922
+ jsonResponse(res, 400, { error: "Invalid request" });
923
+ }
924
+ return;
925
+ }
926
+ // Apply parsed rules to the active governance context
927
+ if (req.url === "/api/apply-rules" && req.method === "POST") {
928
+ try {
929
+ const body = await readBody(req);
930
+ const payload = JSON.parse(body);
931
+ if (!payload.rules || !Array.isArray(payload.rules)) {
932
+ jsonResponse(res, 400, { error: "rules array is required" });
933
+ return;
934
+ }
935
+ // Store custom rules in memory for the session
936
+ customGuards.length = 0;
937
+ for (const rule of payload.rules) {
938
+ customGuards.push({
939
+ id: rule.id,
940
+ label: rule.description,
941
+ description: rule.description,
942
+ category: "custom",
943
+ enforcement: rule.enforcement,
944
+ immutable: false,
945
+ intent_patterns: rule.intent_patterns,
946
+ default_enabled: true,
947
+ });
948
+ }
949
+ jsonResponse(res, 200, {
950
+ status: "applied",
951
+ applied: customGuards.length,
952
+ enforcement: "full",
953
+ detail: "Rules enforced across guard engine (intent blocking) AND invariant engine (system dynamics)",
954
+ });
955
+ }
956
+ catch (err) {
957
+ jsonResponse(res, 400, { error: "Invalid request" });
958
+ }
959
+ return;
960
+ }
961
+ // ── Session Reporting Endpoints ──
962
+ // Connect the serve runtime to the enforce reporting pipeline.
963
+ // Users can request reports, stats, and recommendations from live governance data.
964
+ // GET /api/session — current session stats (lightweight)
965
+ if (req.url === "/api/session" && req.method === "GET") {
966
+ const evals = currentSession.evaluations;
967
+ const blocked = evals.filter(e => e.decision === "BLOCK").length;
968
+ const modified = evals.filter(e => e.decision === "MODIFY").length;
969
+ const allowed = evals.filter(e => e.decision === "ALLOW").length;
970
+ const uniqueActors = [...new Set(evals.map(e => e.actor))];
971
+ const uniqueActions = [...new Set(evals.map(e => e.action))];
972
+ const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
973
+ jsonResponse(res, 200, {
974
+ sessionId: currentSession.id,
975
+ startedAt: currentSession.startedAt,
976
+ world: currentSession.world,
977
+ guardCount: currentSession.guardCount,
978
+ evaluations: {
979
+ total: evals.length,
980
+ blocked,
981
+ modified,
982
+ allowed,
983
+ },
984
+ agents: uniqueActors,
985
+ actionTypes: uniqueActions,
986
+ triggeredRules,
987
+ historyCount: sessionHistory.length,
988
+ });
989
+ return;
990
+ }
991
+ // GET /api/session/report — full enforcement report (text)
992
+ if (req.url === "/api/session/report" && req.method === "GET") {
993
+ const report = synthesizeSessionReport();
994
+ // Format as human-readable text using the same style as enforce CLI
995
+ const lines = [];
996
+ lines.push("");
997
+ lines.push(" LIVE GOVERNANCE — ENFORCEMENT REPORT");
998
+ lines.push(" " + "=".repeat(70));
999
+ lines.push(` Session: ${report.sessionId}`);
1000
+ lines.push(` Scenario: ${report.scenario}`);
1001
+ lines.push(` Sessions: ${report.runs.length}`);
1002
+ lines.push(` Generated: ${report.generatedAt}`);
1003
+ lines.push("");
1004
+ if (report.runs.length > 0) {
1005
+ lines.push(" RUN HISTORY");
1006
+ lines.push(" " + "-".repeat(70));
1007
+ lines.push(` ${"#".padEnd(4)} ${"World".padEnd(25)} ${"Rules".padEnd(8)} ${"Evals".padEnd(8)} ${"Blocked".padEnd(10)} ${"Effectiveness"}`);
1008
+ lines.push(" " + "-".repeat(70));
1009
+ for (const run of report.runs) {
1010
+ const name = run.worldName.length > 23 ? run.worldName.slice(0, 22) + "…" : run.worldName;
1011
+ lines.push(` ${String(run.iteration).padEnd(4)} ${name.padEnd(25)} ${String(run.ruleCount).padEnd(8)} ${String(run.governanceStats.totalEvaluations).padEnd(8)} ${String(run.governanceStats.verdicts.block).padEnd(10)} ${(run.comparison.governanceEffectiveness * 100).toFixed(0)}%`);
1012
+ }
1013
+ lines.push("");
1014
+ lines.push(" DIVERGENCE ANALYSIS");
1015
+ lines.push(" " + "-".repeat(70));
1016
+ lines.push(` Stability trend: ${report.divergence.stabilityTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
1017
+ lines.push(` Collapse trend: ${report.divergence.collapseTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
1018
+ lines.push(` Effectiveness trend: ${report.divergence.effectivenessTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
1019
+ lines.push(` Total divergence: ${report.divergence.totalDivergence}`);
1020
+ lines.push("");
1021
+ lines.push(` ${report.divergence.narrative}`);
1022
+ // Blocked action breakdown
1023
+ const allEvals = [
1024
+ ...sessionHistory.flatMap(s => s.evaluations),
1025
+ ...currentSession.evaluations,
1026
+ ];
1027
+ const blockedEvals = allEvals.filter(e => e.decision === "BLOCK");
1028
+ if (blockedEvals.length > 0) {
1029
+ lines.push("");
1030
+ lines.push(" BLOCKED ACTIONS");
1031
+ lines.push(" " + "-".repeat(70));
1032
+ // Group by rule
1033
+ const byRule = {};
1034
+ for (const e of blockedEvals) {
1035
+ const key = e.ruleId ?? "unknown";
1036
+ if (!byRule[key])
1037
+ byRule[key] = { count: 0, actors: new Set(), actions: new Set() };
1038
+ byRule[key].count++;
1039
+ byRule[key].actors.add(e.actor);
1040
+ byRule[key].actions.add(e.action);
1041
+ }
1042
+ const sorted = Object.entries(byRule).sort((a, b) => b[1].count - a[1].count);
1043
+ for (const [ruleId, data] of sorted) {
1044
+ lines.push(` ${ruleId}: ${data.count} blocks across ${data.actors.size} agent(s)`);
1045
+ lines.push(` Actions: ${[...data.actions].join(", ")}`);
1046
+ }
1047
+ }
1048
+ // Recommendations (deterministic, no LLM needed)
1049
+ lines.push("");
1050
+ lines.push(" RECOMMENDATIONS");
1051
+ lines.push(" " + "-".repeat(70));
1052
+ const best = report.runs[report.divergence.bestIteration - 1];
1053
+ if (best) {
1054
+ const totalEvals = best.governanceStats.totalEvaluations;
1055
+ const blockRate = totalEvals > 0 ? best.governanceStats.verdicts.block / totalEvals : 0;
1056
+ if (blockRate > 0.5) {
1057
+ lines.push(" HIGH BLOCK RATE — Rules may be too aggressive.");
1058
+ lines.push(" Try relaxing constraints or adding MODIFY actions instead of hard blocks.");
1059
+ lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
1060
+ }
1061
+ else if (blockRate < 0.05 && totalEvals > 20) {
1062
+ lines.push(" LOW BLOCK RATE — Rules may be too permissive.");
1063
+ lines.push(" Try adding content-specific guards or lowering thresholds.");
1064
+ lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
1065
+ }
1066
+ else if (blockRate > 0) {
1067
+ lines.push(` Governance is active: ${(blockRate * 100).toFixed(0)}% of actions blocked.`);
1068
+ lines.push(" To iterate: apply different rules via POST /api/apply-rules, then POST /api/session/reset.");
1069
+ lines.push(" This creates a new session for side-by-side comparison.");
1070
+ }
1071
+ if (report.runs.length === 1) {
1072
+ lines.push("");
1073
+ lines.push(" EXPERIMENT: Apply different rules and reset to compare sessions.");
1074
+ lines.push(" This will show divergence: how different governance changes outcomes.");
1075
+ }
1076
+ }
1077
+ lines.push("");
1078
+ lines.push(" RECOMMENDATION");
1079
+ lines.push(" " + "-".repeat(70));
1080
+ lines.push(` ${report.recommendation}`);
1081
+ }
1082
+ lines.push("");
1083
+ lines.push(" " + "=".repeat(70));
1084
+ lines.push(" NeuroVerse Policy Enforcement System — Live Governance");
1085
+ lines.push(" Design rules. Run reality. See what changes.");
1086
+ lines.push(" " + "=".repeat(70));
1087
+ lines.push("");
1088
+ res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
1089
+ res.end(lines.join("\n"));
1090
+ return;
1091
+ }
1092
+ // GET /api/session/report.json — full enforcement report (JSON)
1093
+ if (req.url === "/api/session/report.json" && req.method === "GET") {
1094
+ const report = synthesizeSessionReport();
1095
+ jsonResponse(res, 200, report);
1096
+ return;
1097
+ }
1098
+ // GET /api/session/evaluations — raw evaluation log
1099
+ if (req.url === "/api/session/evaluations" && req.method === "GET") {
1100
+ jsonResponse(res, 200, {
1101
+ sessionId: currentSession.id,
1102
+ count: currentSession.evaluations.length,
1103
+ evaluations: currentSession.evaluations.slice(-200), // last 200
1104
+ });
1105
+ return;
1106
+ }
1107
+ // POST /api/session/reset — snapshot current session and start fresh
1108
+ // This is how users create multi-run comparisons:
1109
+ // 1. Apply rules A → run simulation → POST /api/session/reset
1110
+ // 2. Apply rules B → run simulation → GET /api/session/report
1111
+ // Now the report shows divergence between rules A and rules B.
1112
+ if (req.url === "/api/session/reset" && req.method === "POST") {
1113
+ if (currentSession.evaluations.length > 0) {
1114
+ sessionHistory.push({ ...currentSession, evaluations: [...currentSession.evaluations] });
1115
+ }
1116
+ const body = await readBody(req).catch(() => "{}");
1117
+ const opts = JSON.parse(body || "{}");
1118
+ currentSession = {
1119
+ id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
1120
+ startedAt: new Date().toISOString(),
1121
+ world: opts.world ?? currentSession.world,
1122
+ guardCount: customGuards.length,
1123
+ evaluations: [],
1124
+ };
1125
+ jsonResponse(res, 200, {
1126
+ status: "reset",
1127
+ newSessionId: currentSession.id,
1128
+ previousSessions: sessionHistory.length,
1129
+ message: `Session reset. ${sessionHistory.length} session(s) in history. Apply new rules and evaluate to compare.`,
1130
+ });
1131
+ return;
1132
+ }
1133
+ // POST /api/session/save — save session as experiment with lineage
1134
+ if (req.url === "/api/session/save" && req.method === "POST") {
1135
+ const report = synthesizeSessionReport();
1136
+ if (report.runs.length === 0) {
1137
+ jsonResponse(res, 400, { error: "No evaluations to save. Send actions to /api/evaluate first." });
1138
+ return;
1139
+ }
1140
+ const experiment = {
1141
+ id: `exp-live-${Date.now().toString(36)}`,
1142
+ savedAt: new Date().toISOString(),
1143
+ scenario: report.scenario,
1144
+ source: "live-governance",
1145
+ sessions: report.runs.length,
1146
+ totalEvaluations: report.runs.reduce((sum, r) => sum + r.governanceStats.totalEvaluations, 0),
1147
+ metrics: {
1148
+ stability: report.runs[report.divergence.bestIteration - 1]?.metrics.stabilityScore ?? 0,
1149
+ effectiveness: report.runs[report.divergence.bestIteration - 1]?.comparison.governanceEffectiveness ?? 0,
1150
+ collapseProbability: report.runs[report.divergence.bestIteration - 1]?.metrics.collapseProbability ?? 0,
1151
+ },
1152
+ divergence: report.divergence,
1153
+ recommendation: report.recommendation,
1154
+ report,
1155
+ };
1156
+ // Save to experiments/ directory
1157
+ try {
1158
+ if (!fs.existsSync("experiments"))
1159
+ fs.mkdirSync("experiments", { recursive: true });
1160
+ const filePath = path.join("experiments", `${experiment.id}.json`);
1161
+ fs.writeFileSync(filePath, JSON.stringify(experiment, null, 2));
1162
+ jsonResponse(res, 200, {
1163
+ status: "saved",
1164
+ experimentId: experiment.id,
1165
+ filePath,
1166
+ metrics: experiment.metrics,
1167
+ message: `Saved to ${filePath}. View with: npx nv-sim enforce --load ${filePath}`,
1168
+ });
1169
+ }
1170
+ catch (err) {
1171
+ jsonResponse(res, 500, { error: "Failed to save experiment", detail: String(err) });
1172
+ }
1173
+ return;
1174
+ }
1175
+ // List available live adapters
1176
+ if (req.url === "/api/adapters" && req.method === "GET") {
1177
+ const adapters = Object.values(liveAdapter_1.ADAPTER_REGISTRY).map(a => ({
1178
+ id: a.id,
1179
+ label: a.label,
1180
+ description: a.description,
1181
+ }));
1182
+ jsonResponse(res, 200, { adapters });
1183
+ return;
1184
+ }
1185
+ // Run simulation via live adapter (external process)
1186
+ if (req.url === "/api/run-live" && req.method === "POST") {
1187
+ if (isRunning) {
1188
+ jsonResponse(res, 409, { error: "Simulation already running" });
1189
+ return;
1190
+ }
1191
+ try {
1192
+ const body = await readBody(req);
1193
+ const payload = JSON.parse(body);
1194
+ const adapter = (0, liveAdapter_1.createAdapter)(payload.adapterId, payload.options);
1195
+ if (!adapter) {
1196
+ jsonResponse(res, 400, { error: `Unknown adapter: ${payload.adapterId}` });
1197
+ return;
1198
+ }
1199
+ isRunning = true;
1200
+ jsonResponse(res, 200, { status: "started", adapter: payload.adapterId });
1201
+ // Resolve world for governance evaluation
1202
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
1203
+ const resolved = resolveWorld(payload.worldId ?? "trading");
1204
+ const world = { ...resolved.world };
1205
+ if (payload.stateOverrides) {
1206
+ const updatedVars = world.state_variables.map(sv => {
1207
+ if (payload.stateOverrides && sv.id in payload.stateOverrides) {
1208
+ return { ...sv, default_value: payload.stateOverrides[sv.id] };
1209
+ }
1210
+ return sv;
1211
+ });
1212
+ world.state_variables = updatedVars;
1213
+ }
1214
+ const request = {
1215
+ scenario: resolved.scenario,
1216
+ stakeholders: resolved.stakeholders,
1217
+ assumptions: resolved.assumptions,
1218
+ constraints: resolved.constraints,
1219
+ depth: resolved.depth,
1220
+ swarm: resolved.swarm,
1221
+ };
1222
+ const nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, world);
1223
+ // Send meta event with adapter source
1224
+ broadcast({
1225
+ type: "meta",
1226
+ source: payload.adapterId,
1227
+ scenario: resolved.scenario,
1228
+ worldThesis: world.thesis,
1229
+ agents: [], // will be populated dynamically from adapter
1230
+ invariants: world.invariants.map(inv => ({ id: inv.id, description: inv.description })),
1231
+ gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
1232
+ narrativeEvents: [],
1233
+ totalRounds: 0,
1234
+ });
1235
+ // Listen for rounds from the adapter
1236
+ adapter.on("round", (liveRound) => {
1237
+ // Build lookup of original verdicts from adapter's adaptation data.
1238
+ // When an external bridge (e.g. MiroFish) already applied governance,
1239
+ // the verdict lives in adaptation.deltas — use it instead of re-evaluating
1240
+ // (re-evaluating the MODIFIED action would return ALLOW, hiding the BLOCK).
1241
+ const adaptationByAgent = new Map();
1242
+ if (liveRound.adaptation?.deltas) {
1243
+ for (const delta of liveRound.adaptation.deltas) {
1244
+ adaptationByAgent.set(delta.agent, {
1245
+ status: delta.decision,
1246
+ reason: delta.reason,
1247
+ ruleId: delta.rule,
1248
+ });
1249
+ }
1250
+ }
1251
+ const reactions = liveRound.agentActions.map(a => {
1252
+ // Priority: per-action verdict > adaptation delta > local re-evaluation
1253
+ const actionVerdict = a.verdict ? { status: a.verdict.status, reason: a.verdict.reason, ruleId: a.verdict.rule } : null;
1254
+ const bridgeVerdict = actionVerdict ?? adaptationByAgent.get(a.agent);
1255
+ let verdict;
1256
+ if (bridgeVerdict) {
1257
+ verdict = bridgeVerdict;
1258
+ }
1259
+ else {
1260
+ const guard = (0, worldBridge_1.evaluateScenarioGuard)({ ...request, scenario: `[R${liveRound.round}] ${a.agent}: ${a.action}` }, nvWorld, { trace: true, level: "standard" });
1261
+ verdict = { status: guard.status, reason: guard.reason, ruleId: guard.ruleId };
1262
+ }
1263
+ return {
1264
+ stakeholder_id: a.agent,
1265
+ reaction: a.action,
1266
+ impact: a.impact,
1267
+ confidence: a.confidence ?? 0.5,
1268
+ trigger: liveRound.source,
1269
+ verdict,
1270
+ };
1271
+ });
1272
+ const interventionCount = reactions.filter(r => r.verdict.status !== "ALLOW").length;
1273
+ broadcast({
1274
+ type: "round",
1275
+ round: liveRound.round,
1276
+ totalRounds: 0, // unknown for live streams
1277
+ phase: "governed",
1278
+ reactions,
1279
+ avgImpact: reactions.reduce((s, r) => s + r.impact, 0) / (reactions.length || 1),
1280
+ maxVolatility: Math.max(0, ...reactions.map(r => Math.abs(r.impact))),
1281
+ dynamics: liveRound.emergentDynamics ?? [],
1282
+ interventionCount,
1283
+ });
1284
+ });
1285
+ adapter.on("complete", () => {
1286
+ isRunning = false;
1287
+ });
1288
+ adapter.on("error", (err) => {
1289
+ broadcast({ type: "complete", result: { error: String(err) } });
1290
+ isRunning = false;
1291
+ });
1292
+ adapter.start().catch(err => {
1293
+ broadcast({ type: "complete", result: { error: String(err) } });
1294
+ isRunning = false;
1295
+ });
1296
+ }
1297
+ catch (err) {
1298
+ jsonResponse(res, 400, { error: "Invalid request body" });
1299
+ }
1300
+ return;
1301
+ }
1302
+ // ── AUDIT TRAIL ENDPOINTS ──
1303
+ // GET /api/audit — summary of current session's audit trail
1304
+ if (req.url === "/api/audit" && req.method === "GET") {
1305
+ jsonResponse(res, 200, auditTrail.summary());
1306
+ return;
1307
+ }
1308
+ // GET /api/audit/entries — all entries from current session (with optional filters)
1309
+ if (req.url?.startsWith("/api/audit/entries") && req.method === "GET") {
1310
+ const url = new URL(req.url, `http://localhost:${port}`);
1311
+ const entries = auditTrail.query({
1312
+ type: url.searchParams.get("type") ?? undefined,
1313
+ agent: url.searchParams.get("agent") ?? undefined,
1314
+ verdict: url.searchParams.get("verdict") ?? undefined,
1315
+ runId: url.searchParams.get("runId") ?? undefined,
1316
+ after: url.searchParams.get("after") ?? undefined,
1317
+ before: url.searchParams.get("before") ?? undefined,
1318
+ });
1319
+ jsonResponse(res, 200, {
1320
+ sessionId: auditTrail.getSessionId(),
1321
+ count: entries.length,
1322
+ entries,
1323
+ });
1324
+ return;
1325
+ }
1326
+ // GET /api/audit/entries/text — human-readable audit trail
1327
+ if (req.url?.startsWith("/api/audit/entries/text") && req.method === "GET") {
1328
+ const entries = auditTrail.readAll();
1329
+ res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
1330
+ res.end((0, auditTrace_1.formatAuditTrail)(entries, { verbose: true }));
1331
+ return;
1332
+ }
1333
+ // GET /api/audit/sessions — list all audit sessions on disk
1334
+ if (req.url === "/api/audit/sessions" && req.method === "GET") {
1335
+ const sessions = (0, auditTrace_1.listAuditSessions)();
1336
+ jsonResponse(res, 200, {
1337
+ sessions: sessions.map((id) => {
1338
+ const trail = (0, auditTrace_1.loadAuditSession)(id);
1339
+ return trail.summary();
1340
+ }),
1341
+ });
1342
+ return;
1343
+ }
1344
+ // GET /api/audit/sessions/:id — load a specific session's audit trail
1345
+ const auditSessionMatch = req.url?.match(/^\/api\/audit\/sessions\/([^/]+)$/);
1346
+ if (auditSessionMatch && req.method === "GET") {
1347
+ const sessionId = decodeURIComponent(auditSessionMatch[1]);
1348
+ const trail = (0, auditTrace_1.loadAuditSession)(sessionId);
1349
+ const entries = trail.readAll();
1350
+ if (entries.length === 0) {
1351
+ jsonResponse(res, 404, { error: `Audit session "${sessionId}" not found or empty` });
1352
+ }
1353
+ else {
1354
+ jsonResponse(res, 200, {
1355
+ ...trail.summary(),
1356
+ entries,
1357
+ });
1358
+ }
1359
+ return;
1360
+ }
1361
+ // GET /api/audit/search — search across ALL sessions
1362
+ if (req.url?.startsWith("/api/audit/search") && req.method === "GET") {
1363
+ const url = new URL(req.url, `http://localhost:${port}`);
1364
+ const results = (0, auditTrace_1.searchAuditTrails)({
1365
+ type: url.searchParams.get("type") ?? undefined,
1366
+ agent: url.searchParams.get("agent") ?? undefined,
1367
+ verdict: url.searchParams.get("verdict") ?? undefined,
1368
+ after: url.searchParams.get("after") ?? undefined,
1369
+ before: url.searchParams.get("before") ?? undefined,
1370
+ });
1371
+ jsonResponse(res, 200, {
1372
+ count: results.length,
1373
+ entries: results,
1374
+ });
1375
+ return;
1376
+ }
1377
+ // Serve the interactive dashboard
1378
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1379
+ res.end(INTERACTIVE_DASHBOARD_HTML);
1380
+ });
1381
+ server.listen(port, () => {
1382
+ onReady(`http://localhost:${port}`);
1383
+ });
1384
+ return { server };
1385
+ }
1386
+ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1387
+ <html lang="en">
1388
+ <head>
1389
+ <meta charset="UTF-8">
1390
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1391
+ <title>NV-SIM — Scenario Control Platform</title>
1392
+ <style>
1393
+ /* ── Theme Variables ── */
1394
+ :root {
1395
+ --bg-primary: #0f0f0f;
1396
+ --bg-secondary: #141414;
1397
+ --bg-surface: #1a1a1a;
1398
+ --bg-elevated: #222;
1399
+ --border: #2a2a2a;
1400
+ --border-subtle: #333;
1401
+ --text-primary: #f0f0f0;
1402
+ --text-secondary: #b0b0b0;
1403
+ --text-muted: #888;
1404
+ --text-faint: #666;
1405
+ --accent: #818cf8;
1406
+ --accent-bg: #1e1e3a;
1407
+ --green: #4ade80;
1408
+ --green-bg: #0a2a14;
1409
+ --red: #f87171;
1410
+ --red-bg: #2d0a0a;
1411
+ --yellow: #fbbf24;
1412
+ --yellow-bg: #2d2006;
1413
+ --blue: #60a5fa;
1414
+ --blue-bg: #1e293b;
1415
+ --purple: #a78bfa;
1416
+ }
1417
+ body.light {
1418
+ --bg-primary: #f5f5f5;
1419
+ --bg-secondary: #eaeaea;
1420
+ --bg-surface: #e0e0e0;
1421
+ --bg-elevated: #d4d4d4;
1422
+ --border: #c0c0c0;
1423
+ --border-subtle: #b0b0b0;
1424
+ --text-primary: #1a1a1a;
1425
+ --text-secondary: #444;
1426
+ --text-muted: #666;
1427
+ --text-faint: #888;
1428
+ --accent: #6366f1;
1429
+ --accent-bg: #e8e8ff;
1430
+ --green: #16a34a;
1431
+ --green-bg: #dcfce7;
1432
+ --red: #dc2626;
1433
+ --red-bg: #fee2e2;
1434
+ --yellow: #ca8a04;
1435
+ --yellow-bg: #fef9c3;
1436
+ --blue: #2563eb;
1437
+ --blue-bg: #dbeafe;
1438
+ --purple: #7c3aed;
1439
+ }
1440
+
1441
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1442
+ body { font-family: 'SF Mono', 'Fira Code', monospace; background: var(--bg-primary); color: var(--text-primary); overflow: hidden; transition: background 0.3s, color 0.3s; }
1443
+ .header { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
1444
+ .header h1 { font-size: 15px; color: var(--text-primary); }
1445
+ .header .sub { font-size: 11px; color: var(--text-muted); margin-left: 12px; }
1446
+ .header-right { display: flex; align-items: center; gap: 12px; }
1447
+ .theme-toggle { background: var(--bg-surface); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: 6px; font-family: inherit; font-size: 11px; cursor: pointer; transition: all 0.2s; }
1448
+ .theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
1449
+ .status { font-size: 11px; padding: 3px 10px; border-radius: 10px; }
1450
+ .status.idle { background: var(--accent-bg); color: var(--accent); }
1451
+ .status.live { background: var(--green-bg); color: var(--green); animation: pulse 2s infinite; }
1452
+ .status.complete { background: var(--accent-bg); color: var(--accent); }
1453
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
1454
+
1455
+ .layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }
1456
+
1457
+ /* LEFT PANEL — Controls */
1458
+ .controls { background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px; }
1459
+ .ctrl-section { margin-bottom: 20px; }
1460
+ .ctrl-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
1461
+
1462
+ .ctrl-row { margin-bottom: 12px; }
1463
+ .ctrl-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; display: flex; justify-content: space-between; }
1464
+ .ctrl-label .val { color: var(--text-primary); font-weight: 600; }
1465
+
1466
+ input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: var(--bg-elevated); border-radius: 2px; outline: none; }
1467
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; }
1468
+ input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: none; }
1469
+
1470
+ select { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
1471
+
1472
+ .toggle-row { display: flex; align-items: center; gap: 8px; }
1473
+ .toggle { position: relative; width: 36px; height: 20px; background: var(--border-subtle); border-radius: 10px; cursor: pointer; transition: background 0.2s; }
1474
+ .toggle.on { background: var(--green); }
1475
+ .toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
1476
+ .toggle.on::after { transform: translateX(16px); }
1477
+ .toggle-label { font-size: 12px; color: var(--text-secondary); }
1478
+
1479
+ .inject-row { display: flex; gap: 6px; margin-bottom: 6px; }
1480
+ .inject-row select { flex: 1; }
1481
+ .inject-row input { width: 50px; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 4px 6px; border-radius: 4px; font-family: inherit; font-size: 12px; text-align: center; }
1482
+ .inject-list { margin-bottom: 8px; }
1483
+ .inject-item { font-size: 11px; color: var(--text-secondary); padding: 3px 6px; background: var(--bg-surface); border-radius: 3px; margin-bottom: 3px; display: flex; justify-content: space-between; }
1484
+ .inject-item .remove { color: var(--red); cursor: pointer; }
1485
+
1486
+ .btn { width: 100%; padding: 10px; border: none; border-radius: 6px; font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s; }
1487
+ .btn-run { background: var(--green); color: #0a0a0a; }
1488
+ .btn-run:hover { background: #22c55e; }
1489
+ .btn-run:disabled { background: var(--bg-elevated); color: var(--text-faint); cursor: not-allowed; }
1490
+ .btn-add { background: var(--bg-elevated); color: var(--accent); padding: 6px; font-size: 11px; }
1491
+ .btn-add:hover { background: var(--accent-bg); }
1492
+
1493
+ .scenario-btn { display: block; width: 100%; padding: 8px 10px; margin-bottom: 4px; background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; color: var(--text-primary); font-family: inherit; font-size: 11px; text-align: left; cursor: pointer; }
1494
+ .scenario-btn:hover { border-color: var(--accent); background: var(--accent-bg); }
1495
+ .scenario-btn .stitle { font-weight: 600; color: var(--text-primary); }
1496
+ .scenario-btn .sdesc { color: var(--text-muted); font-size: 10px; margin-top: 2px; }
1497
+
1498
+ /* Save variant */
1499
+ .btn-save { background: var(--accent-bg); color: var(--accent); margin-top: 0; }
1500
+ .btn-save:hover { background: var(--accent-bg); filter: brightness(1.2); }
1501
+ .btn-confirm { flex: 1; background: var(--green); color: #0a0a0a; padding: 7px; font-size: 11px; }
1502
+ .btn-cancel { flex: 1; background: var(--bg-elevated); color: var(--text-muted); padding: 7px; font-size: 11px; }
1503
+ .save-input { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 7px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
1504
+ .save-input:focus { border-color: var(--accent); outline: none; }
1505
+
1506
+ /* Variant cards */
1507
+ .variant-card { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; padding: 8px 10px; margin-bottom: 4px; cursor: pointer; position: relative; }
1508
+ .variant-card:hover { border-color: var(--green); }
1509
+ .variant-card .vname { font-size: 12px; font-weight: 600; color: var(--text-primary); }
1510
+ .variant-card .vdesc { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
1511
+ .variant-card .vmeta { font-size: 10px; color: var(--text-faint); margin-top: 4px; }
1512
+ .variant-card .vmeta .vresult { color: var(--green); }
1513
+ .variant-card .vdelete { position: absolute; top: 6px; right: 8px; color: var(--red); font-size: 10px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
1514
+ .variant-card:hover .vdelete { opacity: 1; }
1515
+ .variant-card .vbase { display: inline-block; font-size: 9px; padding: 1px 5px; background: var(--accent-bg); color: var(--accent); border-radius: 3px; margin-top: 3px; }
1516
+
1517
+ /* RIGHT PANEL — Simulation viewer */
1518
+ .viewer { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
1519
+ .viewer-top { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); }
1520
+ .viewer-mid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); overflow: hidden; }
1521
+ .viewer-bottom { background: var(--bg-primary); border-top: 1px solid var(--border); padding: 12px 16px; max-height: 180px; overflow-y: auto; }
1522
+
1523
+ .vpanel { background: var(--bg-primary); padding: 14px; overflow-y: auto; }
1524
+ .vpanel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
1525
+
1526
+ /* Metrics */
1527
+ .metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
1528
+ .metric-box { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 10px; text-align: center; }
1529
+ .metric-box .value { font-size: 20px; font-weight: 700; color: var(--text-primary); }
1530
+ .metric-box .label { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
1531
+ .metric-box.good .value { color: var(--green); }
1532
+ .metric-box.bad .value { color: var(--red); }
1533
+ .metric-box.warn .value { color: var(--yellow); }
1534
+
1535
+ /* Agent bars */
1536
+ .agent-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; }
1537
+ .agent-name { width: 130px; color: var(--text-secondary); flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1538
+ .impact-bar-bg { flex: 1; height: 14px; background: var(--bg-surface); border-radius: 3px; position: relative; overflow: hidden; }
1539
+ .impact-bar { height: 100%; border-radius: 3px; transition: width 0.4s ease; position: absolute; top: 0; }
1540
+ .impact-bar.positive { background: var(--green); right: 50%; }
1541
+ .impact-bar.negative { background: var(--red); left: 50%; }
1542
+ .impact-val { width: 44px; text-align: right; color: var(--text-secondary); font-size: 10px; }
1543
+ .center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
1544
+ .verdict { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-left: 4px; }
1545
+ .verdict.ALLOW { background: var(--green-bg); color: var(--green); }
1546
+ .verdict.BLOCK { background: var(--red-bg); color: var(--red); }
1547
+ .verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
1548
+
1549
+ /* Chart */
1550
+ .chart-container { position: relative; height: 100%; min-height: 150px; }
1551
+ canvas { width: 100% !important; height: 100% !important; }
1552
+
1553
+ /* Simulation Trace */
1554
+ .trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
1555
+ .trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
1556
+ .trace-round-header:hover { background: var(--bg-elevated); }
1557
+ .trace-phase { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
1558
+ .trace-phase.baseline { background: var(--blue-bg); color: var(--blue); }
1559
+ .trace-phase.governed { background: var(--green-bg); color: var(--green); }
1560
+ .trace-round-label { font-size: 11px; color: var(--text-primary); font-weight: 600; }
1561
+ .trace-round-metrics { margin-left: auto; font-size: 10px; color: var(--text-muted); display: flex; gap: 10px; }
1562
+ .trace-body { padding: 0 10px 8px; }
1563
+ .trace-body[data-collapsed="true"] { display: none; }
1564
+ .trace-section { margin-top: 6px; }
1565
+ .trace-section-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 3px; display: flex; align-items: center; gap: 5px; }
1566
+ .trace-section-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
1567
+ .trace-section-label.event::before { background: #f59e0b; }
1568
+ .trace-section-label.agents::before { background: #3b82f6; }
1569
+ .trace-section-label.governance::before { background: #10b981; }
1570
+ .trace-event-item { font-size: 10px; color: var(--text-primary); padding: 3px 0 3px 11px; border-left: 2px solid #f59e0b; margin-left: 2px; }
1571
+ .trace-event-severity { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; margin-left: 4px; }
1572
+ .trace-event-severity.major, .trace-event-severity.extreme { background: var(--red-bg); color: var(--red); }
1573
+ .trace-event-severity.moderate { background: var(--yellow-bg); color: var(--yellow); }
1574
+ .trace-event-severity.minor { background: var(--green-bg); color: var(--green); }
1575
+ .trace-agent-item { font-size: 10px; padding: 2px 0 2px 11px; border-left: 2px solid #3b82f6; margin-left: 2px; display: flex; align-items: center; gap: 6px; }
1576
+ .trace-agent-name { color: var(--blue); font-weight: 500; min-width: 80px; }
1577
+ .trace-agent-action { color: var(--text-secondary); flex: 1; }
1578
+ .trace-agent-impact { font-size: 9px; font-weight: 600; min-width: 36px; text-align: right; }
1579
+ .trace-agent-impact.positive { color: var(--green); }
1580
+ .trace-agent-impact.negative { color: var(--red); }
1581
+ .trace-gov-item { font-size: 10px; padding: 3px 6px 3px 11px; border-left: 2px solid #10b981; margin-left: 2px; display: flex; align-items: center; gap: 6px; }
1582
+ .trace-gov-rule { font-size: 9px; color: var(--text-muted); font-family: monospace; }
1583
+ .trace-gov-reason { color: var(--text-secondary); flex: 1; }
1584
+ .trace-dynamics { font-size: 10px; color: var(--purple); padding: 2px 0 2px 11px; border-left: 2px solid #7c3aed; margin-left: 2px; font-style: italic; }
1585
+ .trace-arrow { color: var(--text-faint); font-size: 10px; text-align: center; padding: 2px 0; }
1586
+ .trace-empty { font-size: 10px; color: var(--text-faint); font-style: italic; padding: 4px 0; }
1587
+
1588
+ /* World info */
1589
+ .world-thesis { font-size: 11px; color: var(--text-secondary); font-style: italic; margin-bottom: 8px; }
1590
+ .rule-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; transition: border-color 0.2s; }
1591
+ .rule-card:hover { border-color: var(--text-muted); }
1592
+ .rule-card.type-invariant { border-left: 4px solid var(--green); }
1593
+ .rule-card.type-gate { border-left: 4px solid var(--red); }
1594
+ .rule-card.type-warning { border-left: 4px solid var(--yellow); }
1595
+ .rule-card.type-modify { border-left: 4px solid #60a5fa; }
1596
+ .rule-card .rule-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
1597
+ .rule-card .rule-icon { font-size: 12px; flex-shrink: 0; }
1598
+ .rule-card .rule-title { font-size: 12px; font-weight: 600; color: var(--text-primary); }
1599
+ .rule-card .rule-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
1600
+ .rule-card .rule-meta { font-size: 10px; color: var(--text-muted); margin-top: 6px; opacity: 0.8; }
1601
+ .rule-card .rule-why { font-size: 10px; color: var(--text-muted); margin-top: 5px; font-style: italic; padding-top: 5px; border-top: 1px solid var(--border); }
1602
+ .rule-card .rule-impact { display: none; font-size: 10px; margin-top: 6px; padding: 5px 8px; border-radius: 4px; background: var(--bg-surface, var(--bg-secondary)); }
1603
+ .rule-card .rule-impact.visible { display: block; }
1604
+ .rule-card .rule-impact .impact-stat { color: var(--text-primary); font-weight: 600; }
1605
+ .rule-card .rule-impact .impact-label { color: var(--text-muted); }
1606
+ .rule-card.user-rule { border-left-color: #818cf8; background: rgba(129, 140, 248, 0.05); }
1607
+ .rule-source-tag { font-size: 9px; font-weight: 700; color: #818cf8; background: rgba(129, 140, 248, 0.15); padding: 1px 5px; border-radius: 3px; margin-left: auto; letter-spacing: 0.5px; }
1608
+
1609
+ /* Empty state */
1610
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-faint); }
1611
+ .empty-state .icon { font-size: 48px; margin-bottom: 12px; }
1612
+ .empty-state .msg { font-size: 13px; color: var(--text-muted); }
1613
+ .empty-state .hint { font-size: 11px; color: var(--text-faint); margin-top: 6px; }
1614
+
1615
+ /* System Shift Card */
1616
+ .system-shift { display: none; margin: 12px 16px; border: 1px solid var(--green-bg); border-radius: 8px; background: var(--bg-secondary); overflow: hidden; animation: fadeInUp 0.4s ease; }
1617
+ .system-shift.visible { display: block; }
1618
+ @keyframes fadeInUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
1619
+ .ss-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background: var(--green-bg); border-bottom: 1px solid var(--green-bg); }
1620
+ .ss-icon { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgba(74,222,128,0.4); }
1621
+ .ss-title { font-size: 11px; font-weight: 700; color: var(--green); text-transform: uppercase; letter-spacing: 1.5px; }
1622
+ .ss-rule { font-size: 13px; font-weight: 600; color: var(--text-primary); padding: 10px 14px 0; }
1623
+ .ss-body { padding: 10px 14px 14px; display: grid; gap: 8px; }
1624
+ .ss-section { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
1625
+ .ss-section-label { font-size: 9px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
1626
+ .ss-adapt-rate { font-size: 20px; font-weight: 700; color: var(--green); }
1627
+ .ss-adapt-desc { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
1628
+ .ss-shift-item { font-size: 11px; color: var(--text-secondary); padding: 2px 0; display: flex; align-items: center; gap: 6px; }
1629
+ .ss-shift-arrow { color: var(--green); font-weight: 600; }
1630
+ .ss-pattern-tag { display: inline-block; font-size: 10px; padding: 2px 8px; background: var(--accent-bg); color: var(--accent); border-radius: 3px; margin-right: 4px; margin-bottom: 4px; }
1631
+ .ss-impact-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--text-secondary); padding: 2px 0; }
1632
+ .ss-impact-delta { color: var(--green); font-weight: 600; }
1633
+ .ss-impact-delta.negative { color: var(--red); }
1634
+ .ss-narrative { font-size: 12px; color: var(--text-primary); line-height: 1.5; font-style: italic; border-left: 2px solid var(--green); padding-left: 10px; }
1635
+ .ss-scale { font-size: 10px; color: var(--text-muted); padding: 0 14px 6px; }
1636
+ .ss-scale strong { color: var(--text-secondary); }
1637
+ .ss-flow { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 10px; color: var(--text-faint); }
1638
+ .ss-flow-arrow { color: var(--green); }
1639
+ .ss-raw-toggle { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: var(--bg-secondary); border: none; border-top: 1px solid var(--border); color: var(--text-muted); font-family: inherit; font-size: 10px; cursor: pointer; width: 100%; text-align: left; transition: color 0.2s; }
1640
+ .ss-raw-toggle:hover { color: var(--text-secondary); }
1641
+ .ss-raw-toggle .arrow { transition: transform 0.2s; }
1642
+ .ss-raw-toggle.open .arrow { transform: rotate(90deg); }
1643
+ .ss-raw-detail { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
1644
+ .ss-raw-detail.open { max-height: 200px; overflow-y: auto; }
1645
+ .ss-raw-list { padding: 6px 12px; }
1646
+ .ss-raw-item { font-size: 10px; color: var(--text-muted); padding: 2px 0; display: flex; gap: 6px; }
1647
+ .ss-raw-item .raw-agent { color: var(--text-secondary); min-width: 100px; }
1648
+ .ss-raw-item .raw-action { color: var(--text-secondary); flex: 1; }
1649
+ .ss-raw-item .raw-verdict { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; }
1650
+ .ss-raw-item .raw-verdict.BLOCK { background: var(--red-bg); color: var(--red); }
1651
+ .ss-raw-item .raw-verdict.MODIFY { background: var(--yellow-bg); color: var(--yellow); }
1652
+
1653
+ /* Integration Quick-Start (in controls panel) */
1654
+ .integrate-section { background: var(--accent-bg); border: 1px solid var(--accent-bg); border-radius: 6px; padding: 10px 12px; margin-top: 8px; }
1655
+ .integrate-section h4 { font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
1656
+ .integrate-code { font-size: 10px; color: var(--text-primary); background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; padding: 8px; overflow-x: auto; white-space: pre; line-height: 1.5; }
1657
+ .integrate-code .kw { color: var(--accent); }
1658
+ .integrate-code .str { color: var(--green); }
1659
+ .integrate-code .comment { color: var(--text-muted); }
1660
+ .integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
1661
+ .integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
1662
+
1663
+ /* Rule editor */
1664
+ .rule-editor { margin-top: 8px; }
1665
+ .rule-input { width: 100%; min-height: 60px; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 12px; resize: vertical; line-height: 1.5; }
1666
+ .rule-input:focus { border-color: var(--accent); outline: none; }
1667
+ .rule-input::placeholder { color: var(--text-faint); }
1668
+ .btn-parse { background: var(--accent-bg); color: var(--accent); margin-top: 6px; padding: 8px; font-size: 11px; }
1669
+ .btn-parse:hover { filter: brightness(1.2); }
1670
+ .btn-parse:disabled { opacity: 0.5; cursor: not-allowed; }
1671
+ .parsed-rules { margin-top: 8px; }
1672
+ .parsed-rule { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; transition: border-color 0.2s; }
1673
+ .parsed-rule:hover { border-color: var(--text-muted); }
1674
+ .parsed-rule.enforcement-block { border-left: 4px solid var(--red); }
1675
+ .parsed-rule.enforcement-allow { border-left: 4px solid var(--green); }
1676
+ .parsed-rule.enforcement-modify { border-left: 4px solid #60a5fa; }
1677
+ .parsed-rule.enforcement-warn { border-left: 4px solid var(--yellow); }
1678
+ .parsed-rule .pr-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
1679
+ .parsed-rule .pr-icon { font-size: 12px; }
1680
+ .parsed-rule .pr-action { font-weight: 700; font-size: 12px; color: var(--text-primary); text-transform: capitalize; }
1681
+ .parsed-rule .pr-desc { color: var(--text-secondary); font-size: 11px; line-height: 1.4; }
1682
+ .parsed-rule .pr-patterns { color: var(--text-muted); font-size: 10px; margin-top: 6px; opacity: 0.8; }
1683
+ .btn-apply-rules { background: var(--green); color: #0a0a0a; margin-top: 6px; padding: 8px; font-size: 11px; }
1684
+ .btn-apply-rules:hover { filter: brightness(0.9); }
1685
+ .rule-status { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
1686
+ .rule-status.success { color: var(--green); }
1687
+ .rule-status.error { color: var(--red); }
1688
+ .rule-examples { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.6; }
1689
+ .rule-examples code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
1690
+ </style>
1691
+ </head>
1692
+ <body>
1693
+ <div class="header">
1694
+ <div style="display:flex;align-items:center">
1695
+ <h1>NV-SIM</h1>
1696
+ <span class="sub">Scenario Control Platform</span>
1697
+ </div>
1698
+ <div class="header-right">
1699
+ <button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">Light Mode</button>
1700
+ <span id="status" class="status idle">Ready</span>
1701
+ </div>
1702
+ </div>
1703
+
1704
+ <div class="layout">
1705
+ <!-- LEFT: CONTROLS -->
1706
+ <div class="controls" id="controls-panel">
1707
+ <!-- Simulation Engine selector -->
1708
+ <div class="ctrl-section">
1709
+ <h3>Simulation Engine</h3>
1710
+ <div class="ctrl-row">
1711
+ <select id="engine-select">
1712
+ <option value="nv-sim" selected>NV-SIM (Built-in)</option>
1713
+ </select>
1714
+ </div>
1715
+ <div id="engine-status" style="font-size:10px;color:#555;margin-top:4px"></div>
1716
+ </div>
1717
+
1718
+ <!-- World selector -->
1719
+ <div class="ctrl-section">
1720
+ <h3>World</h3>
1721
+ <div class="ctrl-row">
1722
+ <select id="world-select"></select>
1723
+ </div>
1724
+ <div id="world-thesis" class="world-thesis"></div>
1725
+ </div>
1726
+
1727
+ <!-- State variables (dynamic sliders) -->
1728
+ <div class="ctrl-section" id="state-vars-section" style="display:none">
1729
+ <h3>World Rules</h3>
1730
+ <div id="state-vars"></div>
1731
+ </div>
1732
+
1733
+ <!-- Plain-English Rule Editor -->
1734
+ <div class="ctrl-section">
1735
+ <h3>Write Rules in Plain English</h3>
1736
+ <div class="rule-editor">
1737
+ <textarea class="rule-input" id="rule-input" placeholder="Type rules in plain English, one per line:&#10;&#10;Block panic selling during high volatility&#10;Limit leverage to 5x maximum&#10;Pause any trade over $10M for review"></textarea>
1738
+ <button class="btn btn-parse" id="parse-rules-btn">Parse Rules</button>
1739
+ <div id="parsed-rules" class="parsed-rules"></div>
1740
+ <div id="rule-status" class="rule-status"></div>
1741
+ <div class="rule-examples">
1742
+ Examples:<br>
1743
+ <code>Block panic selling</code><br>
1744
+ <code>Limit leverage to 3x</code><br>
1745
+ <code>Pause large trades for review</code><br>
1746
+ <code>Allow hedging positions</code><br>
1747
+ <code>Block short selling during circuit breaker</code>
1748
+ </div>
1749
+ </div>
1750
+ </div>
1751
+
1752
+ <!-- Scenario presets -->
1753
+ <div class="ctrl-section">
1754
+ <h3>Scenario Presets</h3>
1755
+ <div id="scenario-list"></div>
1756
+ </div>
1757
+
1758
+ <!-- Narrative injection -->
1759
+ <div class="ctrl-section">
1760
+ <h3>Narrative Events</h3>
1761
+ <div class="inject-row">
1762
+ <select id="event-select"></select>
1763
+ <input type="number" id="event-round" min="1" max="20" value="3" placeholder="R">
1764
+ </div>
1765
+ <button class="btn btn-add" id="add-event-btn">+ Add Event</button>
1766
+ <div id="inject-list" class="inject-list" style="margin-top:8px"></div>
1767
+ </div>
1768
+
1769
+ <!-- Rounds -->
1770
+ <div class="ctrl-section">
1771
+ <h3>Simulation</h3>
1772
+ <div class="ctrl-row">
1773
+ <div class="ctrl-label">
1774
+ <span>Rounds</span>
1775
+ <span class="val" id="rounds-val">5</span>
1776
+ </div>
1777
+ <input type="range" id="rounds-slider" min="3" max="12" value="5">
1778
+ </div>
1779
+ </div>
1780
+
1781
+ <!-- Run button -->
1782
+ <button class="btn btn-run" id="run-btn">Run Simulation</button>
1783
+
1784
+ <!-- Save as variant -->
1785
+ <div id="save-section" style="margin-top:12px">
1786
+ <button class="btn btn-save" id="save-btn">Save as World Variant</button>
1787
+ <div id="save-form" style="display:none;margin-top:8px">
1788
+ <input type="text" id="variant-name" placeholder="Variant name (e.g. Hormuz Closed + 3x Leverage)" class="save-input">
1789
+ <input type="text" id="variant-desc" placeholder="What does this variant test?" class="save-input" style="margin-top:4px">
1790
+ <div style="display:flex;gap:6px;margin-top:6px">
1791
+ <button class="btn btn-confirm" id="confirm-save-btn">Save</button>
1792
+ <button class="btn btn-cancel" id="cancel-save-btn">Cancel</button>
1793
+ </div>
1794
+ </div>
1795
+ </div>
1796
+
1797
+ <!-- Saved variants -->
1798
+ <div class="ctrl-section" style="margin-top:16px">
1799
+ <h3>Saved Variants</h3>
1800
+ <div id="variant-list"><div style="font-size:11px;color:#333">No saved variants yet</div></div>
1801
+ </div>
1802
+
1803
+ <!-- Integration Quick-Start -->
1804
+ <div class="ctrl-section" style="margin-top:16px">
1805
+ <h3>Integrate Your Simulator</h3>
1806
+ <div class="integrate-section">
1807
+ <h4>Connect in 3 lines</h4>
1808
+ <div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate
1809
+
1810
+ verdict = evaluate(
1811
+ actor=<span class="str">"agent_1"</span>,
1812
+ action=<span class="str">"panic_sell"</span>,
1813
+ world=<span class="str">"trading"</span>
1814
+ )
1815
+
1816
+ <span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
1817
+ action = <span class="str">"hold"</span> <span class="comment"># adapted</span></div>
1818
+ <div class="integrate-endpoint">
1819
+ Endpoint: <code id="integrate-url">POST /api/evaluate</code>
1820
+ </div>
1821
+ <div style="font-size:10px;color:#444;margin-top:6px">
1822
+ Fail-open · 500ms timeout · Stateless
1823
+ </div>
1824
+ <div style="margin-top:8px;font-size:10px">
1825
+ <span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> replaced
1826
+ <span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> constrained
1827
+ <span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
1828
+ </div>
1829
+ </div>
1830
+
1831
+ <!-- Session Report Panel -->
1832
+ <div class="ctrl-section" id="session-panel">
1833
+ <h3 class="ctrl-label">SESSION</h3>
1834
+ <div class="metric-grid" style="grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
1835
+ <div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
1836
+ <div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
1837
+ <div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
1838
+ <div class="metric-box"><div class="value" id="s-allowed" style="color:#4ade80">0</div><div class="label">Allowed</div></div>
1839
+ </div>
1840
+ <div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
1841
+ <div style="display:flex;gap:6px;flex-wrap:wrap">
1842
+ <button onclick="viewSessionReport()" style="flex:1;padding:6px 8px;background:#1e1b4b;color:#818cf8;border:1px solid #312e81;border-radius:4px;cursor:pointer;font-size:11px">View Report</button>
1843
+ <button onclick="resetSession()" style="flex:1;padding:6px 8px;background:#1c1917;color:#a8a29e;border:1px solid #292524;border-radius:4px;cursor:pointer;font-size:11px">Reset &amp; Compare</button>
1844
+ <button onclick="saveExperiment()" style="flex:1;padding:6px 8px;background:#052e16;color:#4ade80;border:1px solid #14532d;border-radius:4px;cursor:pointer;font-size:11px">Save</button>
1845
+ </div>
1846
+ <div id="session-history" style="margin-top:6px;font-size:10px;color:#666"></div>
1847
+ </div>
1848
+ </div>
1849
+ </div>
1850
+
1851
+ <!-- RIGHT: VIEWER -->
1852
+ <div class="viewer">
1853
+ <div class="viewer-top">
1854
+ <div class="vpanel">
1855
+ <h2>Live Metrics</h2>
1856
+ <div class="metric-grid">
1857
+ <div class="metric-box"><div class="value" id="m-stability">--</div><div class="label">Stability</div></div>
1858
+ <div class="metric-box"><div class="value" id="m-volatility">--</div><div class="label">Volatility</div></div>
1859
+ <div class="metric-box"><div class="value" id="m-round">--</div><div class="label">Round</div></div>
1860
+ <div class="metric-box"><div class="value" id="m-interventions">0</div><div class="label">Interventions</div></div>
1861
+ </div>
1862
+ </div>
1863
+ <div class="vpanel">
1864
+ <h2>World Rules Active</h2>
1865
+ <div id="active-invariants"></div>
1866
+ </div>
1867
+ </div>
1868
+
1869
+ <div class="viewer-mid">
1870
+ <div class="vpanel" id="agents-panel">
1871
+ <h2>Agent Impacts</h2>
1872
+ <div id="agents">
1873
+ <div class="empty-state"><div class="icon">&gt;_</div><div class="msg">Configure world and run simulation</div><div class="hint">Adjust rules on the left, then press Run</div></div>
1874
+ </div>
1875
+ </div>
1876
+ <div class="vpanel">
1877
+ <h2>Impact Timeline</h2>
1878
+ <div class="chart-container"><canvas id="chart"></canvas></div>
1879
+ </div>
1880
+ </div>
1881
+
1882
+ <!-- System Shift Card — the demo moment -->
1883
+ <div id="system-shift" class="system-shift">
1884
+ <div class="ss-header">
1885
+ <div class="ss-icon"></div>
1886
+ <span class="ss-title">System Shift</span>
1887
+ </div>
1888
+ <div class="ss-rule" id="ss-rule"></div>
1889
+ <div class="ss-scale" id="ss-scale"></div>
1890
+ <div class="ss-flow">
1891
+ <span>Rule</span><span class="ss-flow-arrow">→</span>
1892
+ <span>Behavioral Shift</span><span class="ss-flow-arrow">→</span>
1893
+ <span>Emergent Pattern</span><span class="ss-flow-arrow">→</span>
1894
+ <span>System Outcome</span>
1895
+ </div>
1896
+ <div class="ss-body">
1897
+ <div class="ss-section">
1898
+ <div class="ss-section-label">Behavioral Shift</div>
1899
+ <div class="ss-adapt-rate" id="ss-adapt-rate"></div>
1900
+ <div class="ss-adapt-desc" id="ss-adapt-desc"></div>
1901
+ <div id="ss-shifts"></div>
1902
+ </div>
1903
+ <div class="ss-section">
1904
+ <div class="ss-section-label">What Emerged</div>
1905
+ <div id="ss-patterns"></div>
1906
+ </div>
1907
+ <div class="ss-section">
1908
+ <div class="ss-section-label">System Outcome</div>
1909
+ <div id="ss-impacts"></div>
1910
+ </div>
1911
+ <div class="ss-section">
1912
+ <div class="ss-section-label">What Actually Happened</div>
1913
+ <div class="ss-narrative" id="ss-narrative"></div>
1914
+ </div>
1915
+ </div>
1916
+ <button class="ss-raw-toggle" id="ss-raw-toggle">
1917
+ <span class="arrow">▶</span> View raw detail
1918
+ </button>
1919
+ <div class="ss-raw-detail" id="ss-raw-detail">
1920
+ <div class="ss-raw-list" id="ss-raw-list"></div>
1921
+ </div>
1922
+ </div>
1923
+
1924
+ <div class="viewer-bottom">
1925
+ <h2 style="font-size:11px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Simulation Trace <span id="trace-source" style="color:#4ade80;font-weight:600;margin-left:6px"></span> <span style="color:#333;font-weight:400">— Events → Agents → Rules → Outcomes</span></h2>
1926
+ <div id="log"></div>
1927
+ </div>
1928
+ </div>
1929
+ </div>
1930
+
1931
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"><\/script>
1932
+ <script>
1933
+ // ============================================
1934
+ // STATE
1935
+ // ============================================
1936
+ let worlds = [];
1937
+ let scenarios = {};
1938
+ let narratives = {};
1939
+ let currentWorld = null;
1940
+ let injectedEvents = [];
1941
+ let totalInterventions = 0;
1942
+ let baselineImpacts = [];
1943
+ let governedImpacts = [];
1944
+ let chartLabels = [];
1945
+ let chart = null;
1946
+ let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
1947
+ let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
1948
+
1949
+ const statusEl = document.getElementById('status');
1950
+ const worldSelect = document.getElementById('world-select');
1951
+ const stateVarsSection = document.getElementById('state-vars-section');
1952
+ const stateVarsEl = document.getElementById('state-vars');
1953
+ const scenarioListEl = document.getElementById('scenario-list');
1954
+ const eventSelect = document.getElementById('event-select');
1955
+ const eventRoundInput = document.getElementById('event-round');
1956
+ const injectListEl = document.getElementById('inject-list');
1957
+ const roundsSlider = document.getElementById('rounds-slider');
1958
+ const roundsVal = document.getElementById('rounds-val');
1959
+ const runBtn = document.getElementById('run-btn');
1960
+ const agentsEl = document.getElementById('agents');
1961
+ const logEl = document.getElementById('log');
1962
+ const activeInvEl = document.getElementById('active-invariants');
1963
+ const engineSelect = document.getElementById('engine-select');
1964
+ const engineStatusEl = document.getElementById('engine-status');
1965
+ const traceSourceEl = document.getElementById('trace-source');
1966
+
1967
+ // ============================================
1968
+ // INIT — Load worlds, scenarios, narratives, adapters
1969
+ // ============================================
1970
+ async function init() {
1971
+ const [wRes, sRes, nRes, aRes] = await Promise.all([
1972
+ fetch('/api/worlds').then(r => r.json()),
1973
+ fetch('/api/scenarios').then(r => r.json()),
1974
+ fetch('/api/narratives').then(r => r.json()),
1975
+ fetch('/api/adapters').then(r => r.json()).catch(() => ({ adapters: [] })),
1976
+ ]);
1977
+
1978
+ worlds = wRes.worlds;
1979
+ scenarios = sRes.scenarios;
1980
+ narratives = nRes.narratives;
1981
+
1982
+ // Populate engine selector with live adapters
1983
+ (aRes.adapters || []).forEach(function(a) {
1984
+ const opt = document.createElement('option');
1985
+ opt.value = a.id;
1986
+ opt.textContent = a.label;
1987
+ engineSelect.appendChild(opt);
1988
+ });
1989
+
1990
+ // Populate world select
1991
+ worlds.forEach(w => {
1992
+ const opt = document.createElement('option');
1993
+ opt.value = w.id;
1994
+ opt.textContent = w.title;
1995
+ worldSelect.appendChild(opt);
1996
+ });
1997
+
1998
+ // Populate event select
1999
+ Object.entries(narratives).forEach(([id, ev]) => {
2000
+ const opt = document.createElement('option');
2001
+ opt.value = id;
2002
+ opt.textContent = ev.headline.slice(0, 40);
2003
+ eventSelect.appendChild(opt);
2004
+ });
2005
+
2006
+ // Populate scenario presets
2007
+ Object.entries(scenarios).forEach(([id, s]) => {
2008
+ const btn = document.createElement('button');
2009
+ btn.className = 'scenario-btn';
2010
+ btn.innerHTML = '<div class="stitle">' + s.title + '</div><div class="sdesc">' + s.description + '</div>';
2011
+ btn.onclick = () => loadScenario(id, s);
2012
+ scenarioListEl.appendChild(btn);
2013
+ });
2014
+
2015
+ // Select first world
2016
+ if (worlds.length > 0) selectWorld(worlds[0].id);
2017
+
2018
+ // Load saved variants
2019
+ await loadVariants();
2020
+
2021
+ // Connect SSE
2022
+ connectSSE();
2023
+ }
2024
+
2025
+ function selectWorld(worldId) {
2026
+ currentWorld = worlds.find(w => w.id === worldId);
2027
+ if (!currentWorld) return;
2028
+
2029
+ worldSelect.value = worldId;
2030
+ document.getElementById('world-thesis').textContent = '"' + currentWorld.thesis + '"';
2031
+
2032
+ // Render state variable controls
2033
+ if (currentWorld.stateVariables && currentWorld.stateVariables.length > 0) {
2034
+ stateVarsSection.style.display = '';
2035
+ stateVarsEl.innerHTML = '';
2036
+ currentWorld.stateVariables.forEach(sv => {
2037
+ const row = document.createElement('div');
2038
+ row.className = 'ctrl-row';
2039
+
2040
+ if (sv.type === 'number' && sv.range) {
2041
+ const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
2042
+ row.innerHTML =
2043
+ '<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
2044
+ '<input type="range" id="sv-' + sv.id + '" min="' + sv.range.min + '" max="' + sv.range.max + '" step="' + step + '" value="' + sv.default_value + '" data-sv="' + sv.id + '">';
2045
+ stateVarsEl.appendChild(row);
2046
+ const slider = row.querySelector('input');
2047
+ slider.addEventListener('input', () => {
2048
+ document.getElementById('sv-val-' + sv.id).textContent = slider.value;
2049
+ });
2050
+ } else if (sv.type === 'enum' && sv.enum_values) {
2051
+ row.innerHTML =
2052
+ '<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
2053
+ '<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
2054
+ sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
2055
+ '</select>';
2056
+ stateVarsEl.appendChild(row);
2057
+ } else if (sv.type === 'boolean') {
2058
+ row.innerHTML =
2059
+ '<div class="toggle-row">' +
2060
+ '<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
2061
+ '<span class="toggle-label">' + sv.label + '</span>' +
2062
+ '</div>';
2063
+ stateVarsEl.appendChild(row);
2064
+ const toggle = row.querySelector('.toggle');
2065
+ toggle.addEventListener('click', () => {
2066
+ toggle.classList.toggle('on');
2067
+ });
2068
+ }
2069
+ });
2070
+ } else {
2071
+ stateVarsSection.style.display = 'none';
2072
+ }
2073
+
2074
+ // Show invariants
2075
+ activeInvEl.innerHTML = currentWorld.invariants.map(inv =>
2076
+ '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
2077
+ ).join('') + (currentWorld.gates || []).map(g =>
2078
+ '<div class="inv-item" style="color:' + (g.severity === 'critical' ? '#f87171' : '#fbbf24') + '">[' + g.id + '] ' + g.label + '</div>'
2079
+ ).join('');
2080
+ }
2081
+
2082
+ function loadScenario(id, scenario) {
2083
+ // Set world
2084
+ selectWorld(scenario.world);
2085
+ // Set events
2086
+ injectedEvents = scenario.events.slice();
2087
+ renderInjectedEvents();
2088
+ // Set rounds
2089
+ const r = scenario.rounds || 5;
2090
+ roundsSlider.value = Math.min(r, 12);
2091
+ roundsVal.textContent = Math.min(r, 12);
2092
+ }
2093
+
2094
+ // ============================================
2095
+ // NARRATIVE EVENT INJECTION
2096
+ // ============================================
2097
+ document.getElementById('add-event-btn').addEventListener('click', () => {
2098
+ const eventId = eventSelect.value;
2099
+ const round = parseInt(eventRoundInput.value);
2100
+ if (!eventId || isNaN(round)) return;
2101
+ injectedEvents.push(eventId + '@' + round);
2102
+ renderInjectedEvents();
2103
+ });
2104
+
2105
+ function renderInjectedEvents() {
2106
+ injectListEl.innerHTML = injectedEvents.map((ev, i) =>
2107
+ '<div class="inject-item"><span>' + ev + '</span><span class="remove" data-idx="' + i + '">x</span></div>'
2108
+ ).join('');
2109
+ injectListEl.querySelectorAll('.remove').forEach(el => {
2110
+ el.addEventListener('click', () => {
2111
+ injectedEvents.splice(parseInt(el.dataset.idx), 1);
2112
+ renderInjectedEvents();
2113
+ });
2114
+ });
2115
+ }
2116
+
2117
+ // ============================================
2118
+ // WORLD SELECT
2119
+ // ============================================
2120
+ worldSelect.addEventListener('change', () => selectWorld(worldSelect.value));
2121
+ roundsSlider.addEventListener('input', () => { roundsVal.textContent = roundsSlider.value; });
2122
+
2123
+ // ============================================
2124
+ // RUN SIMULATION
2125
+ // ============================================
2126
+ runBtn.addEventListener('click', async () => {
2127
+ if (!currentWorld) return;
2128
+ runBtn.disabled = true;
2129
+ runBtn.textContent = 'Running...';
2130
+
2131
+ // Reset viewer state
2132
+ totalInterventions = 0;
2133
+ baselineImpacts = [];
2134
+ governedImpacts = [];
2135
+ chartLabels = [];
2136
+ if (chart) { chart.destroy(); chart = null; }
2137
+ agentsEl.innerHTML = '';
2138
+ logEl.innerHTML = '';
2139
+ document.getElementById('m-stability').textContent = '--';
2140
+ document.getElementById('m-volatility').textContent = '--';
2141
+ document.getElementById('m-round').textContent = '--';
2142
+ document.getElementById('m-interventions').textContent = '0';
2143
+
2144
+ // Gather state overrides
2145
+ const stateOverrides = {};
2146
+ if (currentWorld.stateVariables) {
2147
+ currentWorld.stateVariables.forEach(sv => {
2148
+ const el = document.getElementById('sv-' + sv.id);
2149
+ if (!el) return;
2150
+ if (sv.type === 'number') stateOverrides[sv.id] = parseFloat(el.value);
2151
+ else if (sv.type === 'boolean') stateOverrides[sv.id] = el.classList.contains('on');
2152
+ else stateOverrides[sv.id] = el.value;
2153
+ });
2154
+ }
2155
+
2156
+ const selectedEngine = engineSelect.value;
2157
+
2158
+ try {
2159
+ if (selectedEngine === 'nv-sim') {
2160
+ // Built-in simulation
2161
+ const config = {
2162
+ worldId: currentWorld.id,
2163
+ stateOverrides,
2164
+ injectEvents: injectedEvents.length > 0 ? injectedEvents : undefined,
2165
+ rounds: parseInt(roundsSlider.value),
2166
+ };
2167
+ await fetch('/api/run-sim', {
2168
+ method: 'POST',
2169
+ headers: { 'Content-Type': 'application/json' },
2170
+ body: JSON.stringify(config),
2171
+ });
2172
+ } else {
2173
+ // Live adapter (external simulator)
2174
+ const payload = {
2175
+ adapterId: selectedEngine,
2176
+ worldId: currentWorld.id,
2177
+ stateOverrides,
2178
+ };
2179
+ await fetch('/api/run-live', {
2180
+ method: 'POST',
2181
+ headers: { 'Content-Type': 'application/json' },
2182
+ body: JSON.stringify(payload),
2183
+ });
2184
+ }
2185
+ } catch (err) {
2186
+ addLog('Error starting simulation: ' + err.message, 'block');
2187
+ runBtn.disabled = false;
2188
+ runBtn.textContent = 'Run Simulation';
2189
+ }
2190
+ });
2191
+
2192
+ // ============================================
2193
+ // SSE — Live stream
2194
+ // ============================================
2195
+ function connectSSE() {
2196
+ const es = new EventSource('/events');
2197
+ es.onmessage = (e) => {
2198
+ try { handleEvent(JSON.parse(e.data)); } catch {}
2199
+ };
2200
+ es.onerror = () => {};
2201
+ }
2202
+
2203
+ function initChart() {
2204
+ if (typeof Chart === 'undefined') return;
2205
+ const ctx = document.getElementById('chart');
2206
+ chart = new Chart(ctx, {
2207
+ type: 'line',
2208
+ data: {
2209
+ labels: chartLabels,
2210
+ datasets: [
2211
+ { label: 'Baseline', data: baselineImpacts, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
2212
+ { label: 'Governed', data: governedImpacts, borderColor: '#4ade80', backgroundColor: 'rgba(74,222,128,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
2213
+ ]
2214
+ },
2215
+ options: {
2216
+ animation: { duration: 400 },
2217
+ responsive: true,
2218
+ maintainAspectRatio: false,
2219
+ plugins: { legend: { labels: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888', font: { family: 'monospace', size: 10 } } } },
2220
+ scales: {
2221
+ x: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' } },
2222
+ y: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' }, min: -1, max: 1 }
2223
+ }
2224
+ }
2225
+ });
2226
+ }
2227
+
2228
+ function addLog(msg, cls) {
2229
+ const entry = document.createElement('div');
2230
+ entry.style.cssText = 'font-size:10px;color:#666;padding:3px 0;';
2231
+ entry.textContent = msg;
2232
+ logEl.prepend(entry);
2233
+ }
2234
+
2235
+ function addTraceRound(event) {
2236
+ const el = document.createElement('div');
2237
+ el.className = 'trace-round';
2238
+
2239
+ const phaseCls = event.phase === 'baseline' ? 'baseline' : 'governed';
2240
+ const volatilityPct = (event.maxVolatility * 100).toFixed(0);
2241
+ el.innerHTML = '<div class="trace-round-header" onclick="this.nextElementSibling.dataset.collapsed = this.nextElementSibling.dataset.collapsed === \\'true\\' ? \\'false\\' : \\'true\\'">' +
2242
+ '<span class="trace-phase ' + phaseCls + '">' + event.phase + '</span>' +
2243
+ '<span class="trace-round-label">Round ' + event.round + ' / ' + event.totalRounds + '</span>' +
2244
+ '<span class="trace-round-metrics">' +
2245
+ '<span>avg: ' + event.avgImpact.toFixed(2) + '</span>' +
2246
+ '<span>vol: ' + volatilityPct + '%</span>' +
2247
+ (event.interventionCount > 0 ? '<span style="color:#fbbf24">' + event.interventionCount + ' interventions</span>' : '') +
2248
+ '</span>' +
2249
+ '</div>';
2250
+
2251
+ let bodyHtml = '';
2252
+
2253
+ // 1) Narrative events injected this round
2254
+ const roundEvents = (typeof narrativeEventsByRound !== 'undefined' && narrativeEventsByRound[event.round]) || [];
2255
+ if (roundEvents.length > 0) {
2256
+ bodyHtml += '<div class="trace-section"><div class="trace-section-label event">Event Injection</div>';
2257
+ roundEvents.forEach(function(ev) {
2258
+ bodyHtml += '<div class="trace-event-item">' + ev.headline + ' <span class="trace-event-severity ' + ev.severity + '">' + ev.severity + '</span></div>';
2259
+ });
2260
+ bodyHtml += '</div><div class="trace-arrow">↓</div>';
2261
+ }
2262
+
2263
+ // 2) Agent reactions (cap at 30 to prevent DOM bloat with thousands of agents)
2264
+ var MAX_TRACE_AGENTS = 30;
2265
+ bodyHtml += '<div class="trace-section"><div class="trace-section-label agents">Agent Reactions (' + event.reactions.length + ')</div>';
2266
+ event.reactions.slice(0, MAX_TRACE_AGENTS).forEach(function(r) {
2267
+ const impactCls = r.impact >= 0 ? 'positive' : 'negative';
2268
+ bodyHtml += '<div class="trace-agent-item">' +
2269
+ '<span class="trace-agent-name">' + r.stakeholder_id + '</span>' +
2270
+ '<span class="trace-agent-action">' + r.reaction + '</span>' +
2271
+ '<span class="trace-agent-impact ' + impactCls + '">' + r.impact.toFixed(2) + '</span>' +
2272
+ '</div>';
2273
+ });
2274
+ if (event.reactions.length > MAX_TRACE_AGENTS) {
2275
+ var traceRemaining = event.reactions.length - MAX_TRACE_AGENTS;
2276
+ bodyHtml += '<div style="font-size:10px;color:#444;padding:3px 0 3px 11px;border-left:2px solid #3b82f6;margin-left:2px">+ ' + traceRemaining + ' more agents</div>';
2277
+ }
2278
+ bodyHtml += '</div>';
2279
+
2280
+ // 3) Governance interventions
2281
+ const governed = event.reactions.filter(function(r) { return r.verdict && r.verdict.status !== 'ALLOW'; });
2282
+ if (governed.length > 0) {
2283
+ bodyHtml += '<div class="trace-arrow">↓</div>';
2284
+ bodyHtml += '<div class="trace-section"><div class="trace-section-label governance">Governance Interventions</div>';
2285
+ governed.forEach(function(r) {
2286
+ bodyHtml += '<div class="trace-gov-item">' +
2287
+ '<span class="verdict ' + r.verdict.status + '">' + r.verdict.status + '</span>' +
2288
+ (r.verdict.ruleId ? '<span class="trace-gov-rule">[' + r.verdict.ruleId + ']</span>' : '') +
2289
+ '<span class="trace-gov-reason">' + r.stakeholder_id + ': ' + (r.verdict.reason || '') + '</span>' +
2290
+ '</div>';
2291
+ });
2292
+ bodyHtml += '</div>';
2293
+ }
2294
+
2295
+ // 4) Emergent dynamics
2296
+ if (event.dynamics && event.dynamics.length > 0) {
2297
+ bodyHtml += '<div class="trace-arrow">↓</div>';
2298
+ bodyHtml += '<div class="trace-section"><div class="trace-section-label" style="color:#7c3aed">Emergent Dynamics</div>';
2299
+ event.dynamics.forEach(function(d) {
2300
+ bodyHtml += '<div class="trace-dynamics">' + d + '</div>';
2301
+ });
2302
+ bodyHtml += '</div>';
2303
+ }
2304
+
2305
+ const bodyEl = document.createElement('div');
2306
+ bodyEl.className = 'trace-body';
2307
+ bodyEl.dataset.collapsed = 'false';
2308
+ bodyEl.innerHTML = bodyHtml;
2309
+ el.appendChild(bodyEl);
2310
+
2311
+ logEl.prepend(el);
2312
+ }
2313
+
2314
+ function renderAgents(reactions) {
2315
+ var MAX_AGENT_ROWS = 50;
2316
+ var shown = reactions.slice(0, MAX_AGENT_ROWS);
2317
+ var html = shown.map(r => {
2318
+ const pct = Math.abs(r.impact) * 50;
2319
+ const cls = r.impact >= 0 ? 'positive' : 'negative';
2320
+ const dir = r.impact >= 0 ? 'right:50%;width:' + pct + '%' : 'left:50%;width:' + pct + '%';
2321
+ const verdictHtml = r.verdict ? ' <span class="verdict ' + r.verdict.status + '">' + r.verdict.status + '</span>' : '';
2322
+ return '<div class="agent-row">' +
2323
+ '<span class="agent-name">' + r.stakeholder_id + verdictHtml + '</span>' +
2324
+ '<div class="impact-bar-bg"><div class="center-line"></div><div class="impact-bar ' + cls + '" style="' + dir + '"></div></div>' +
2325
+ '<span class="impact-val">' + r.impact.toFixed(2) + '</span>' +
2326
+ '</div>';
2327
+ }).join('');
2328
+ if (reactions.length > MAX_AGENT_ROWS) {
2329
+ var remaining = reactions.length - MAX_AGENT_ROWS;
2330
+ var avgImpact = reactions.slice(MAX_AGENT_ROWS).reduce(function(s, r) { return s + r.impact; }, 0) / remaining;
2331
+ html += '<div class="agent-row" style="color:#555;font-size:10px;justify-content:center">+ ' + remaining + ' more agents (avg impact: ' + avgImpact.toFixed(2) + ')</div>';
2332
+ }
2333
+ agentsEl.innerHTML = html;
2334
+ }
2335
+
2336
+ function handleEvent(event) {
2337
+ if (event.type === 'meta') {
2338
+ statusEl.className = 'status live';
2339
+ statusEl.textContent = 'LIVE';
2340
+ // Show simulation source
2341
+ const src = event.source || 'nv-sim';
2342
+ if (src !== 'nv-sim') {
2343
+ traceSourceEl.textContent = '● ' + src.toUpperCase() + ' (LIVE)';
2344
+ traceSourceEl.style.color = '#4ade80';
2345
+ engineStatusEl.textContent = 'Streaming from ' + src;
2346
+ engineStatusEl.style.color = '#4ade80';
2347
+ } else {
2348
+ traceSourceEl.textContent = '';
2349
+ engineStatusEl.textContent = '';
2350
+ }
2351
+ addLog('Simulation started: ' + event.agents.length + ' agents, ' + event.totalRounds + ' rounds' + (src !== 'nv-sim' ? ' [source: ' + src + ']' : ''));
2352
+ resetShiftTracker();
2353
+ // Store narrative events by round for trace rendering
2354
+ narrativeEventsByRound = {};
2355
+ (event.narrativeEvents || []).forEach(function(ev) {
2356
+ if (!narrativeEventsByRound[ev.round]) narrativeEventsByRound[ev.round] = [];
2357
+ narrativeEventsByRound[ev.round].push(ev);
2358
+ });
2359
+ function parseRuleTitle(id, text) {
2360
+ var parts = text.split(/\s*[—–-]{1,}\s*/);
2361
+ var title = parts.length > 1 ? parts[0].trim() : id.replace(/[-_]/g, ' ');
2362
+ var desc = parts.length > 1 ? parts.slice(1).join(' — ').trim() : text;
2363
+ title = title.replace(/\b\w/g, function(c) { return c.toUpperCase(); });
2364
+ return { title: title, desc: desc };
2365
+ }
2366
+ function generateWhy(text, type) {
2367
+ var t = text.toLowerCase();
2368
+ if (t.includes('liquidity') || t.includes('drain')) return 'Prevents system collapse from liquidity crises';
2369
+ if (t.includes('panic') || t.includes('cascade')) return 'Stops runaway feedback loops during market shocks';
2370
+ if (t.includes('leverage')) return 'Limits systemic risk from over-leveraged positions';
2371
+ if (t.includes('price') || t.includes('pricing')) return 'Stabilizes pricing mechanisms during volatility';
2372
+ if (t.includes('sentiment') || t.includes('consumer')) return 'Tracks behavioral feedback between price and confidence';
2373
+ if (t.includes('military') || t.includes('escalat')) return 'Models how escalation constrains available options';
2374
+ if (t.includes('diplomatic') || t.includes('window')) return 'Preserves negotiation pathways before they close';
2375
+ if (t.includes('grid') || t.includes('capacity')) return 'Prevents infrastructure overload from demand surges';
2376
+ if (t.includes('election') || t.includes('political')) return 'Captures how political pressure amplifies responses';
2377
+ if (t.includes('supply') || t.includes('energy') || t.includes('oil')) return 'Guards against cascading supply chain disruptions';
2378
+ if (t.includes('fraud') || t.includes('suspicious')) return 'Detects and contains anomalous behavior patterns';
2379
+ if (t.includes('withdraw') || t.includes('limit')) return 'Constrains individual actions to protect system stability';
2380
+ if (type === 'gate') return 'Blocks dangerous actions before they propagate';
2381
+ if (type === 'warning') return 'Provides early warning before thresholds are breached';
2382
+ return 'Maintains system integrity under stress conditions';
2383
+ }
2384
+ // Reset per-rule impact counters
2385
+ ruleImpactTracker = {};
2386
+ activeInvEl.innerHTML = event.invariants.map(function(inv) {
2387
+ var parsed = parseRuleTitle(inv.id, inv.description);
2388
+ var why = generateWhy(inv.description, 'invariant');
2389
+ var isUser = inv.source === 'user';
2390
+ var isFull = inv.enforcement === 'full';
2391
+ var enfLabel = isFull ? 'Fully enforced across system' : 'Advisory only';
2392
+ var enfIcon = isFull ? '&#x2713;' : '&#x26A0;';
2393
+ var sourceTag = isUser ? '<span class="rule-source-tag">USER RULE</span>' : '';
2394
+ ruleImpactTracker[inv.id] = { blocks: 0, label: parsed.title };
2395
+ return '<div class="rule-card type-invariant' + (isUser ? ' user-rule' : '') + '" data-rule-id="' + inv.id + '">' +
2396
+ '<div class="rule-header"><span class="rule-icon">&#x1F7E2;</span><span class="rule-title">' + parsed.title + '</span>' + sourceTag + '</div>' +
2397
+ '<div class="rule-desc">' + parsed.desc + '</div>' +
2398
+ '<div class="rule-meta">Invariant &bull; ' + enfIcon + ' ' + enfLabel + '</div>' +
2399
+ '<div class="rule-why">' + why + '</div>' +
2400
+ '<div class="rule-impact" data-impact-id="' + inv.id + '"></div>' +
2401
+ '</div>';
2402
+ }).join('') + event.gates.map(function(g) {
2403
+ var isCritical = g.severity === 'critical';
2404
+ var typeClass = isCritical ? 'type-gate' : 'type-warning';
2405
+ var icon = isCritical ? '&#x1F534;' : '&#x1F7E1;';
2406
+ var typeLabel = isCritical ? 'Gate' : 'Warning';
2407
+ var effect = isCritical ? 'Blocks actions' : 'Signals risk';
2408
+ var why = generateWhy(g.label + ' ' + (g.condition || ''), isCritical ? 'gate' : 'warning');
2409
+ ruleImpactTracker[g.id] = { blocks: 0, label: g.label };
2410
+ return '<div class="rule-card ' + typeClass + '" data-rule-id="' + g.id + '">' +
2411
+ '<div class="rule-header"><span class="rule-icon">' + icon + '</span><span class="rule-title">' + g.label + '</span></div>' +
2412
+ '<div class="rule-desc">' + (g.condition || g.label) + '</div>' +
2413
+ '<div class="rule-meta">' + typeLabel + ' &bull; ' + effect + '</div>' +
2414
+ '<div class="rule-why">' + why + '</div>' +
2415
+ '<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
2416
+ '</div>';
2417
+ }).join('');
2418
+ initChart();
2419
+ }
2420
+
2421
+ if (event.type === 'round') {
2422
+ if (event.phase === 'baseline') {
2423
+ chartLabels.push('R' + event.round);
2424
+ baselineImpacts.push(event.avgImpact);
2425
+ } else {
2426
+ governedImpacts.push(event.avgImpact);
2427
+ }
2428
+ if (chart) chart.update();
2429
+
2430
+ renderAgents(event.reactions);
2431
+ document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
2432
+ document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
2433
+ document.getElementById('m-volatility').parentElement.className = 'metric-box ' + (event.maxVolatility > 0.6 ? 'bad' : event.maxVolatility > 0.4 ? 'warn' : 'good');
2434
+
2435
+ totalInterventions += event.interventionCount;
2436
+ document.getElementById('m-interventions').textContent = totalInterventions;
2437
+
2438
+ // Track system shifts for the card
2439
+ trackShift(event);
2440
+
2441
+ // Render structured trace entry instead of flat log line
2442
+ addTraceRound(event);
2443
+ }
2444
+
2445
+ if (event.type === 'complete') {
2446
+ statusEl.className = 'status complete';
2447
+ statusEl.textContent = 'COMPLETE';
2448
+ const r = event.result;
2449
+ if (r.governed) {
2450
+ document.getElementById('m-stability').textContent = (r.governed.metrics.stabilityScore * 100).toFixed(0) + '%';
2451
+ document.getElementById('m-stability').parentElement.className = 'metric-box ' + (r.governed.metrics.stabilityScore > 0.7 ? 'good' : 'warn');
2452
+ addLog('Complete. Governance effectiveness: ' + (r.comparison.governanceEffectiveness * 100).toFixed(0) + '%');
2453
+ renderSystemShift(r);
2454
+ renderRuleImpacts(r);
2455
+ renderEnforcementClassification(r.enforcementClassification || []);
2456
+ lastSimResult = {
2457
+ stability: r.governed.metrics.stabilityScore,
2458
+ volatility: r.governed.metrics.maxVolatility,
2459
+ collapseProbability: r.governed.metrics.collapseProbability,
2460
+ governanceEffectiveness: r.comparison.governanceEffectiveness,
2461
+ };
2462
+ }
2463
+ runBtn.disabled = false;
2464
+ runBtn.textContent = 'Run Simulation';
2465
+ }
2466
+ }
2467
+
2468
+ // ============================================
2469
+ // PER-RULE IMPACT RENDERING
2470
+ // ============================================
2471
+ function renderRuleImpacts(result) {
2472
+ var totalBlocks = shiftTracker.blocks;
2473
+ var cascadeAvoided = result && result.governed && result.baseline &&
2474
+ result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
2475
+
2476
+ Object.keys(ruleImpactTracker).forEach(function(ruleId) {
2477
+ var tracker = ruleImpactTracker[ruleId];
2478
+ var el = document.querySelector('[data-impact-id="' + ruleId + '"]');
2479
+ if (!el) return;
2480
+
2481
+ var html = '';
2482
+ if (tracker.blocks > 0) {
2483
+ html += '<span class="impact-stat">' + tracker.blocks + ' action' + (tracker.blocks > 1 ? 's' : '') + ' blocked</span>';
2484
+ if (cascadeAvoided) {
2485
+ html += ' <span class="impact-label">&rarr; cascade avoided</span>';
2486
+ } else {
2487
+ html += ' <span class="impact-label">&rarr; behavior modified</span>';
2488
+ }
2489
+ } else {
2490
+ // Rule was present but didn't fire — still useful info
2491
+ html += '<span class="impact-label">No violations detected &mdash; agents complied</span>';
2492
+ }
2493
+ el.innerHTML = html;
2494
+ el.classList.add('visible');
2495
+ });
2496
+ }
2497
+
2498
+ // ============================================
2499
+ // ENFORCEMENT CLASSIFICATION — Full vs Advisory
2500
+ // ============================================
2501
+ function renderEnforcementClassification(entries) {
2502
+ if (!entries || entries.length === 0) return;
2503
+ entries.forEach(function(entry) {
2504
+ var card = document.querySelector('[data-rule-id="' + entry.id + '"]');
2505
+ if (!card) return;
2506
+ var metaEl = card.querySelector('.rule-meta');
2507
+ if (!metaEl) return;
2508
+ if (entry.level === 'full' && entry.fired) {
2509
+ metaEl.innerHTML = 'Invariant &bull; &#x2713; Fully enforced &bull; <span style="color:#22c55e;font-weight:600">FIRED</span>';
2510
+ card.style.borderLeftColor = '#22c55e';
2511
+ } else if (entry.level === 'full' && !entry.fired) {
2512
+ metaEl.innerHTML = 'Invariant &bull; &#x2713; Fully enforced &bull; <span style="color:var(--text-muted)">standby</span>';
2513
+ } else if (entry.level === 'advisory') {
2514
+ metaEl.innerHTML = 'Invariant &bull; &#x26A0; Advisory only &bull; <span style="color:var(--text-muted)">monitored</span>';
2515
+ card.style.borderLeftColor = '#6b7280';
2516
+ card.style.opacity = '0.75';
2517
+ }
2518
+ });
2519
+ }
2520
+
2521
+ // ============================================
2522
+ // SYSTEM SHIFT CARD — The Demo Moment
2523
+ // ============================================
2524
+ const ssCard = document.getElementById('system-shift');
2525
+ const ssRuleEl = document.getElementById('ss-rule');
2526
+ const ssAdaptRateEl = document.getElementById('ss-adapt-rate');
2527
+ const ssAdaptDescEl = document.getElementById('ss-adapt-desc');
2528
+ const ssShiftsEl = document.getElementById('ss-shifts');
2529
+ const ssPatternsEl = document.getElementById('ss-patterns');
2530
+ const ssImpactsEl = document.getElementById('ss-impacts');
2531
+ const ssNarrativeEl = document.getElementById('ss-narrative');
2532
+
2533
+ // Raw detail toggle
2534
+ document.getElementById('ss-raw-toggle').addEventListener('click', function() {
2535
+ this.classList.toggle('open');
2536
+ document.getElementById('ss-raw-detail').classList.toggle('open');
2537
+ });
2538
+
2539
+ let shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
2540
+
2541
+ function resetShiftTracker() {
2542
+ shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
2543
+ ssCard.classList.remove('visible');
2544
+ var rawToggle = document.getElementById('ss-raw-toggle');
2545
+ var rawDetail = document.getElementById('ss-raw-detail');
2546
+ if (rawToggle) rawToggle.classList.remove('open');
2547
+ if (rawDetail) rawDetail.classList.remove('open');
2548
+ }
2549
+
2550
+ function trackShift(event) {
2551
+ if (!event.reactions) return;
2552
+ const governed = event.reactions.filter(function(r) { return r.verdict && r.verdict.status !== 'ALLOW'; });
2553
+ shiftTracker.blocks += governed.length;
2554
+ shiftTracker.total += event.reactions.length;
2555
+ governed.forEach(function(r) {
2556
+ const key = r.verdict.status + ': ' + (r.reaction || 'adapted');
2557
+ shiftTracker.shifts[key] = (shiftTracker.shifts[key] || 0) + 1;
2558
+ // Track per-rule impacts for card display
2559
+ var ruleId = r.verdict.ruleId || '';
2560
+ if (ruleId && ruleImpactTracker[ruleId]) {
2561
+ ruleImpactTracker[ruleId].blocks++;
2562
+ } else {
2563
+ // Try to match by pattern — governance may report with NV- prefix
2564
+ Object.keys(ruleImpactTracker).forEach(function(k) {
2565
+ if (ruleId.includes(k) || (r.verdict.reason && r.verdict.reason.toLowerCase().includes(ruleImpactTracker[k].label.toLowerCase()))) {
2566
+ ruleImpactTracker[k].blocks++;
2567
+ }
2568
+ });
2569
+ }
2570
+ // Store raw governed reactions for detail view
2571
+ shiftTracker.rawGoverned.push({
2572
+ agent: r.stakeholder_id,
2573
+ action: r.reaction,
2574
+ status: r.verdict.status,
2575
+ reason: r.verdict.reason || '',
2576
+ round: event.round,
2577
+ });
2578
+ });
2579
+ if (event.phase === 'baseline') shiftTracker.baselineVol = Math.max(shiftTracker.baselineVol, event.maxVolatility || 0);
2580
+ if (event.phase === 'governed') shiftTracker.governedVol = Math.max(shiftTracker.governedVol, event.maxVolatility || 0);
2581
+ if (event.dynamics) {
2582
+ event.dynamics.forEach(function(d) {
2583
+ if (shiftTracker.patterns.indexOf(d) === -1) shiftTracker.patterns.push(d);
2584
+ });
2585
+ }
2586
+ }
2587
+
2588
+ function renderSystemShift(result) {
2589
+ if (shiftTracker.blocks === 0) return;
2590
+
2591
+ var adaptRate = shiftTracker.total > 0 ? Math.round((shiftTracker.blocks / shiftTracker.total) * 100) : 0;
2592
+
2593
+ // Determine rule label from world
2594
+ var ruleName = 'Governance Rules Active';
2595
+ if (currentWorld && currentWorld.invariants) {
2596
+ var names = currentWorld.invariants.map(function(inv) { return inv.description || inv.id; });
2597
+ ruleName = names.slice(0, 2).join(' + ');
2598
+ }
2599
+
2600
+ ssRuleEl.textContent = ruleName;
2601
+ ssAdaptRateEl.textContent = adaptRate + '%';
2602
+ ssAdaptDescEl.textContent = 'adaptation across ' + shiftTracker.total.toLocaleString() + ' agents';
2603
+
2604
+ // Scale line — proves this is real
2605
+ var scaleEl = document.getElementById('ss-scale');
2606
+ scaleEl.innerHTML = '<strong>' + shiftTracker.blocks.toLocaleString() + '</strong> actions reshaped out of <strong>' + shiftTracker.total.toLocaleString() + '</strong> total';
2607
+
2608
+ // Shifts — show top 5 by count, collapse the rest
2609
+ var shiftHtml = '';
2610
+ var shiftKeys = Object.keys(shiftTracker.shifts).sort(function(a, b) {
2611
+ return shiftTracker.shifts[b] - shiftTracker.shifts[a];
2612
+ });
2613
+ var MAX_SHIFTS = 5;
2614
+ shiftKeys.slice(0, MAX_SHIFTS).forEach(function(k) {
2615
+ shiftHtml += '<div class="ss-shift-item"><span class="ss-shift-arrow">→</span> ' + k + ' (' + shiftTracker.shifts[k] + ' agents)</div>';
2616
+ });
2617
+ if (shiftKeys.length > MAX_SHIFTS) {
2618
+ var remaining = shiftKeys.slice(MAX_SHIFTS).reduce(function(sum, k) { return sum + shiftTracker.shifts[k]; }, 0);
2619
+ shiftHtml += '<div class="ss-shift-item" style="color:#555">+ ' + (shiftKeys.length - MAX_SHIFTS) + ' more patterns (' + remaining + ' agents)</div>';
2620
+ }
2621
+ ssShiftsEl.innerHTML = shiftHtml;
2622
+
2623
+ // Patterns
2624
+ var patternHtml = '';
2625
+ if (shiftTracker.patterns.length > 0) {
2626
+ shiftTracker.patterns.forEach(function(p) {
2627
+ patternHtml += '<span class="ss-pattern-tag">' + p + '</span>';
2628
+ });
2629
+ } else {
2630
+ // Infer patterns from data
2631
+ if (shiftTracker.blocks >= 3) patternHtml += '<span class="ss-pattern-tag">Coordinated Holding</span>';
2632
+ if (shiftTracker.blocks >= 2) patternHtml += '<span class="ss-pattern-tag">Panic Suppression</span>';
2633
+ if (shiftTracker.governedVol < shiftTracker.baselineVol) patternHtml += '<span class="ss-pattern-tag">Stability Shift</span>';
2634
+ }
2635
+ ssPatternsEl.innerHTML = patternHtml || '<span style="font-size:11px;color:#555">No emergent patterns detected</span>';
2636
+
2637
+ // Impact
2638
+ var impactHtml = '';
2639
+ var volDelta = shiftTracker.governedVol - shiftTracker.baselineVol;
2640
+ impactHtml += '<div class="ss-impact-row"><span>Volatility</span><span class="ss-impact-delta' + (volDelta > 0 ? ' negative' : '') + '">' +
2641
+ (shiftTracker.baselineVol * 100).toFixed(0) + '% → ' + (shiftTracker.governedVol * 100).toFixed(0) + '%</span></div>';
2642
+
2643
+ if (result && result.governed && result.baseline) {
2644
+ var stabDelta = result.governed.metrics.stabilityScore - result.baseline.metrics.stabilityScore;
2645
+ impactHtml += '<div class="ss-impact-row"><span>Stability</span><span class="ss-impact-delta">' +
2646
+ (result.baseline.metrics.stabilityScore * 100).toFixed(0) + '% → ' + (result.governed.metrics.stabilityScore * 100).toFixed(0) + '%</span></div>';
2647
+
2648
+ var cascadeAvoided = result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
2649
+ if (cascadeAvoided) {
2650
+ impactHtml += '<div class="ss-impact-row"><span>Cascade</span><span class="ss-impact-delta">Avoided</span></div>';
2651
+ }
2652
+ }
2653
+ ssImpactsEl.innerHTML = impactHtml;
2654
+
2655
+ // Narrative — tight cause → shift → outcome
2656
+ var topShift = shiftKeys.length > 0 ? shiftKeys[0].split(': ')[1] || 'adapted' : 'adapted';
2657
+ var narrative = '';
2658
+
2659
+ // What was the rule?
2660
+ narrative += ruleName + '. ';
2661
+
2662
+ // What shifted?
2663
+ narrative += shiftTracker.blocks + ' of ' + shiftTracker.total + ' agents reorganized';
2664
+ if (topShift !== 'adapted') narrative += ' — most shifted to ' + topShift;
2665
+ narrative += '. ';
2666
+
2667
+ // What emerged?
2668
+ if (shiftTracker.patterns.length > 0) {
2669
+ narrative += 'Pattern: ' + shiftTracker.patterns.slice(0, 2).join(', ') + '. ';
2670
+ }
2671
+
2672
+ // What was the outcome?
2673
+ if (volDelta < 0) narrative += 'Volatility dropped ' + Math.abs(volDelta * 100).toFixed(0) + '%.';
2674
+ else if (volDelta === 0) narrative += 'System held steady.';
2675
+ else narrative += 'Volatility increased ' + (volDelta * 100).toFixed(0) + '% — rules need tuning.';
2676
+
2677
+ ssNarrativeEl.textContent = narrative;
2678
+
2679
+ // Raw detail — virtualized list of governed actions (collapsed by default)
2680
+ var rawListEl = document.getElementById('ss-raw-list');
2681
+ var MAX_RAW = 100;
2682
+ var rawItems = shiftTracker.rawGoverned.slice(0, MAX_RAW);
2683
+ var rawHtml = rawItems.map(function(r) {
2684
+ return '<div class="ss-raw-item">' +
2685
+ '<span class="raw-agent">' + r.agent + '</span>' +
2686
+ '<span class="raw-action">R' + r.round + ': ' + r.action + '</span>' +
2687
+ '<span class="raw-verdict ' + r.status + '">' + r.status + '</span>' +
2688
+ '</div>';
2689
+ }).join('');
2690
+ if (shiftTracker.rawGoverned.length > MAX_RAW) {
2691
+ rawHtml += '<div style="font-size:10px;color:#444;padding:4px 0">+ ' + (shiftTracker.rawGoverned.length - MAX_RAW) + ' more governed actions</div>';
2692
+ }
2693
+ rawListEl.innerHTML = rawHtml;
2694
+
2695
+ ssCard.classList.add('visible');
2696
+ }
2697
+
2698
+ // ============================================
2699
+ // WORLD VARIANTS — Save / Load / Delete
2700
+ // ============================================
2701
+ const variantListEl = document.getElementById('variant-list');
2702
+ const saveBtn = document.getElementById('save-btn');
2703
+ const saveForm = document.getElementById('save-form');
2704
+ const confirmSaveBtn = document.getElementById('confirm-save-btn');
2705
+ const cancelSaveBtn = document.getElementById('cancel-save-btn');
2706
+ const variantNameInput = document.getElementById('variant-name');
2707
+ const variantDescInput = document.getElementById('variant-desc');
2708
+ let lastSimResult = null;
2709
+
2710
+ saveBtn.addEventListener('click', () => {
2711
+ saveForm.style.display = saveForm.style.display === 'none' ? '' : 'none';
2712
+ variantNameInput.focus();
2713
+ });
2714
+
2715
+ cancelSaveBtn.addEventListener('click', () => {
2716
+ saveForm.style.display = 'none';
2717
+ });
2718
+
2719
+ confirmSaveBtn.addEventListener('click', async () => {
2720
+ const name = variantNameInput.value.trim();
2721
+ if (!name || !currentWorld) return;
2722
+
2723
+ // Gather current state overrides
2724
+ const stateOverrides = {};
2725
+ if (currentWorld.stateVariables) {
2726
+ currentWorld.stateVariables.forEach(sv => {
2727
+ const el = document.getElementById('sv-' + sv.id);
2728
+ if (!el) return;
2729
+ if (sv.type === 'number') stateOverrides[sv.id] = parseFloat(el.value);
2730
+ else if (sv.type === 'boolean') stateOverrides[sv.id] = el.classList.contains('on');
2731
+ else stateOverrides[sv.id] = el.value;
2732
+ });
2733
+ }
2734
+
2735
+ const payload = {
2736
+ name,
2737
+ description: variantDescInput.value.trim(),
2738
+ baseWorld: currentWorld.id,
2739
+ stateOverrides,
2740
+ events: injectedEvents.slice(),
2741
+ rounds: parseInt(roundsSlider.value),
2742
+ lastResult: lastSimResult,
2743
+ };
2744
+
2745
+ try {
2746
+ const resp = await fetch('/api/save-variant', {
2747
+ method: 'POST',
2748
+ headers: { 'Content-Type': 'application/json' },
2749
+ body: JSON.stringify(payload),
2750
+ });
2751
+ const data = await resp.json();
2752
+ if (data.status === 'saved') {
2753
+ addLog('Variant saved: ' + data.variant.name + ' (' + data.variant.id + ')', '');
2754
+ saveForm.style.display = 'none';
2755
+ variantNameInput.value = '';
2756
+ variantDescInput.value = '';
2757
+ await loadVariants();
2758
+ }
2759
+ } catch (err) {
2760
+ addLog('Error saving variant: ' + err.message, 'block');
2761
+ }
2762
+ });
2763
+
2764
+ async function loadVariants() {
2765
+ try {
2766
+ const resp = await fetch('/api/variants');
2767
+ const data = await resp.json();
2768
+ renderVariants(data.variants || []);
2769
+ } catch {}
2770
+ }
2771
+
2772
+ function renderVariants(variants) {
2773
+ if (variants.length === 0) {
2774
+ variantListEl.innerHTML = '<div style="font-size:11px;color:#333">No saved variants yet</div>';
2775
+ return;
2776
+ }
2777
+ variantListEl.innerHTML = variants.map(v => {
2778
+ const resultHtml = v.lastResult
2779
+ ? '<span class="vresult">Stability: ' + (v.lastResult.stability * 100).toFixed(0) + '% | Effectiveness: ' + (v.lastResult.governanceEffectiveness * 100).toFixed(0) + '%</span>'
2780
+ : '<span style="color:#555">Not yet run</span>';
2781
+ return '<div class="variant-card" data-vid="' + v.id + '">' +
2782
+ '<div class="vname">' + v.name + '</div>' +
2783
+ (v.description ? '<div class="vdesc">' + v.description + '</div>' : '') +
2784
+ '<span class="vbase">' + v.baseWorld + '</span>' +
2785
+ '<div class="vmeta">' + resultHtml + ' | ' + v.events.length + ' events | ' + v.rounds + ' rounds</div>' +
2786
+ '<span class="vdelete" data-vid="' + v.id + '">delete</span>' +
2787
+ '</div>';
2788
+ }).join('');
2789
+
2790
+ // Bind load handlers
2791
+ variantListEl.querySelectorAll('.variant-card').forEach(card => {
2792
+ card.addEventListener('click', (e) => {
2793
+ if (e.target.classList.contains('vdelete')) return;
2794
+ const vid = card.dataset.vid;
2795
+ const v = variants.find(x => x.id === vid);
2796
+ if (v) loadVariant(v);
2797
+ });
2798
+ });
2799
+
2800
+ // Bind delete handlers
2801
+ variantListEl.querySelectorAll('.vdelete').forEach(el => {
2802
+ el.addEventListener('click', async (e) => {
2803
+ e.stopPropagation();
2804
+ const vid = el.dataset.vid;
2805
+ if (!confirm('Delete variant "' + vid + '"?')) return;
2806
+ try {
2807
+ await fetch('/api/delete-variant/' + vid, { method: 'DELETE' });
2808
+ addLog('Variant deleted: ' + vid, '');
2809
+ await loadVariants();
2810
+ } catch {}
2811
+ });
2812
+ });
2813
+ }
2814
+
2815
+ function loadVariant(variant) {
2816
+ // Set world
2817
+ selectWorld(variant.baseWorld);
2818
+
2819
+ // Apply state overrides
2820
+ if (variant.stateOverrides && currentWorld && currentWorld.stateVariables) {
2821
+ currentWorld.stateVariables.forEach(sv => {
2822
+ if (sv.id in variant.stateOverrides) {
2823
+ const el = document.getElementById('sv-' + sv.id);
2824
+ if (!el) return;
2825
+ if (sv.type === 'boolean') {
2826
+ const val = variant.stateOverrides[sv.id];
2827
+ if (val && !el.classList.contains('on')) el.classList.add('on');
2828
+ if (!val && el.classList.contains('on')) el.classList.remove('on');
2829
+ } else {
2830
+ el.value = variant.stateOverrides[sv.id];
2831
+ // Update value display for sliders
2832
+ const valEl = document.getElementById('sv-val-' + sv.id);
2833
+ if (valEl) valEl.textContent = variant.stateOverrides[sv.id];
2834
+ }
2835
+ }
2836
+ });
2837
+ }
2838
+
2839
+ // Set events
2840
+ injectedEvents = variant.events.slice();
2841
+ renderInjectedEvents();
2842
+
2843
+ // Set rounds
2844
+ roundsSlider.value = Math.min(variant.rounds, 12);
2845
+ roundsVal.textContent = Math.min(variant.rounds, 12);
2846
+
2847
+ addLog('Loaded variant: ' + variant.name, '');
2848
+ }
2849
+
2850
+ // ============================================
2851
+ // CAPTURE LAST RESULT FOR VARIANT SAVING
2852
+ // ============================================
2853
+ // (override handleEvent to capture results)
2854
+ const _origHandleEvent = handleEvent;
2855
+ handleEvent = function(event) {
2856
+ _origHandleEvent(event);
2857
+ if (event.type === 'complete' && event.result && event.result.governed) {
2858
+ lastSimResult = {
2859
+ stability: event.result.governed.metrics.stabilityScore,
2860
+ volatility: event.result.governed.metrics.maxVolatility,
2861
+ collapseProbability: event.result.governed.metrics.collapseProbability,
2862
+ governanceEffectiveness: event.result.comparison.governanceEffectiveness,
2863
+ };
2864
+ }
2865
+ };
2866
+
2867
+ // ============================================
2868
+ // THEME TOGGLE — Light / Dark mode
2869
+ // ============================================
2870
+ const themeToggleBtn = document.getElementById('theme-toggle');
2871
+ function applyTheme(theme) {
2872
+ if (theme === 'light') {
2873
+ document.body.classList.add('light');
2874
+ themeToggleBtn.textContent = 'Dark Mode';
2875
+ } else {
2876
+ document.body.classList.remove('light');
2877
+ themeToggleBtn.textContent = 'Light Mode';
2878
+ }
2879
+ localStorage.setItem('nv-theme', theme);
2880
+ // Update chart colors if chart exists
2881
+ if (chart && chart.options) {
2882
+ const gridColor = theme === 'light' ? '#d4d4d4' : '#2a2a2a';
2883
+ const tickColor = theme === 'light' ? '#666' : '#888';
2884
+ const legendColor = theme === 'light' ? '#444' : '#888';
2885
+ chart.options.scales.x.ticks.color = tickColor;
2886
+ chart.options.scales.x.grid.color = gridColor;
2887
+ chart.options.scales.y.ticks.color = tickColor;
2888
+ chart.options.scales.y.grid.color = gridColor;
2889
+ chart.options.plugins.legend.labels.color = legendColor;
2890
+ chart.update();
2891
+ }
2892
+ }
2893
+ themeToggleBtn.addEventListener('click', () => {
2894
+ const current = document.body.classList.contains('light') ? 'light' : 'dark';
2895
+ applyTheme(current === 'light' ? 'dark' : 'light');
2896
+ });
2897
+ // Restore saved theme
2898
+ const savedTheme = localStorage.getItem('nv-theme');
2899
+ if (savedTheme) applyTheme(savedTheme);
2900
+
2901
+ // ============================================
2902
+ // PLAIN-ENGLISH RULE EDITOR
2903
+ // ============================================
2904
+ const ruleInput = document.getElementById('rule-input');
2905
+ const parseRulesBtn = document.getElementById('parse-rules-btn');
2906
+ const parsedRulesEl = document.getElementById('parsed-rules');
2907
+ const ruleStatusEl = document.getElementById('rule-status');
2908
+ let parsedRuleData = [];
2909
+
2910
+ parseRulesBtn.addEventListener('click', async () => {
2911
+ const text = ruleInput.value.trim();
2912
+ if (!text) return;
2913
+
2914
+ parseRulesBtn.disabled = true;
2915
+ parseRulesBtn.textContent = 'Parsing...';
2916
+ ruleStatusEl.textContent = '';
2917
+ ruleStatusEl.className = 'rule-status';
2918
+
2919
+ try {
2920
+ const resp = await fetch('/api/parse-rules', {
2921
+ method: 'POST',
2922
+ headers: { 'Content-Type': 'application/json' },
2923
+ body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
2924
+ });
2925
+ const data = await resp.json();
2926
+
2927
+ if (data.error) {
2928
+ ruleStatusEl.textContent = data.error;
2929
+ ruleStatusEl.className = 'rule-status error';
2930
+ parsedRulesEl.innerHTML = '';
2931
+ parsedRuleData = [];
2932
+ } else {
2933
+ parsedRuleData = data.rules || [];
2934
+ parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
2935
+ const enfType = r.enforcement || 'block';
2936
+ const iconMap = { block: '&#x1F534;', allow: '&#x1F7E2;', modify: '&#x1F535;', warn: '&#x1F7E1;', pause: '&#x1F7E1;' };
2937
+ const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
2938
+ const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
2939
+ const icon = iconMap[enfType] || '&#x1F7E2;';
2940
+ const label = labelMap[enfType] || 'Rule';
2941
+ const effect = effectMap[enfType] || 'Active';
2942
+ return '<div class="parsed-rule enforcement-' + enfType + '">' +
2943
+ '<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
2944
+ '<div class="pr-desc">' + r.description + '</div>' +
2945
+ '<div class="pr-patterns">' + effect + ' &bull; Matches: ' + r.intent_patterns.join(', ') + '</div>' +
2946
+ '</div>';
2947
+ }).join('');
2948
+
2949
+ if (parsedRuleData.length > 0) {
2950
+ parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation</button>';
2951
+ document.getElementById('apply-rules-btn').addEventListener('click', async () => {
2952
+ try {
2953
+ const applyResp = await fetch('/api/apply-rules', {
2954
+ method: 'POST',
2955
+ headers: { 'Content-Type': 'application/json' },
2956
+ body: JSON.stringify({ rules: parsedRuleData, worldId: currentWorld ? currentWorld.id : 'trading' }),
2957
+ });
2958
+ const applyData = await applyResp.json();
2959
+ if (applyData.status === 'applied') {
2960
+ ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
2961
+ ruleStatusEl.className = 'rule-status success';
2962
+ }
2963
+ } catch (err) {
2964
+ ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
2965
+ ruleStatusEl.className = 'rule-status error';
2966
+ }
2967
+ });
2968
+ ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click Apply.';
2969
+ ruleStatusEl.className = 'rule-status success';
2970
+ }
2971
+ }
2972
+ } catch (err) {
2973
+ ruleStatusEl.textContent = 'Error: ' + err.message;
2974
+ ruleStatusEl.className = 'rule-status error';
2975
+ }
2976
+
2977
+ parseRulesBtn.disabled = false;
2978
+ parseRulesBtn.textContent = 'Parse Rules';
2979
+ });
2980
+
2981
+ // ============================================
2982
+ // SESSION TRACKING
2983
+ // ============================================
2984
+
2985
+ let sessionPollInterval = null;
2986
+
2987
+ async function pollSessionStats() {
2988
+ try {
2989
+ const resp = await fetch('/api/session');
2990
+ const data = await resp.json();
2991
+ const el = (id) => document.getElementById(id);
2992
+ if (el('s-total')) el('s-total').textContent = data.evaluations.total;
2993
+ if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
2994
+ if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
2995
+ if (el('s-allowed')) el('s-allowed').textContent = data.evaluations.allowed;
2996
+ if (el('s-agents')) {
2997
+ el('s-agents').textContent = data.agents.length > 0
2998
+ ? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
2999
+ : 'No agents connected yet';
3000
+ }
3001
+ if (el('session-history') && data.historyCount > 0) {
3002
+ el('session-history').textContent = data.historyCount + ' previous session(s) saved for comparison';
3003
+ }
3004
+ } catch {}
3005
+ }
3006
+
3007
+ async function viewSessionReport() {
3008
+ try {
3009
+ const resp = await fetch('/api/session/report');
3010
+ const text = await resp.text();
3011
+ const log = document.getElementById('sim-log');
3012
+ if (log) {
3013
+ const div = document.createElement('div');
3014
+ div.className = 'log-round';
3015
+ div.innerHTML = '<h4 style="color:#818cf8">Enforcement Report</h4><pre style="white-space:pre-wrap;font-size:11px;color:#d4d4d8">' + text.replace(/</g,'&lt;') + '</pre>';
3016
+ log.prepend(div);
3017
+ log.scrollTop = 0;
3018
+ }
3019
+ } catch (err) { console.error('Failed to load report', err); }
3020
+ }
3021
+
3022
+ async function resetSession() {
3023
+ if (!confirm('Reset session? Current data will be saved for comparison.')) return;
3024
+ try {
3025
+ const resp = await fetch('/api/session/reset', { method: 'POST' });
3026
+ const data = await resp.json();
3027
+ const log = document.getElementById('sim-log');
3028
+ if (log) {
3029
+ const div = document.createElement('div');
3030
+ div.className = 'log-round';
3031
+ div.innerHTML = '<h4 style="color:#fbbf24">Session Reset</h4><div style="font-size:11px;color:#a8a29e">' + data.message + '</div>';
3032
+ log.prepend(div);
3033
+ }
3034
+ pollSessionStats();
3035
+ } catch (err) { console.error('Failed to reset session', err); }
3036
+ }
3037
+
3038
+ async function saveExperiment() {
3039
+ try {
3040
+ const resp = await fetch('/api/session/save', { method: 'POST' });
3041
+ const data = await resp.json();
3042
+ if (data.error) { alert(data.error); return; }
3043
+ const log = document.getElementById('sim-log');
3044
+ if (log) {
3045
+ const div = document.createElement('div');
3046
+ div.className = 'log-round';
3047
+ div.innerHTML = '<h4 style="color:#4ade80">Experiment Saved</h4><div style="font-size:11px;color:#a8a29e">ID: ' + data.experimentId + '<br>File: ' + data.filePath + '<br>Stability: ' + (data.metrics.stability * 100).toFixed(0) + '% | Effectiveness: ' + (data.metrics.effectiveness * 100).toFixed(0) + '%</div>';
3048
+ log.prepend(div);
3049
+ }
3050
+ } catch (err) { console.error('Failed to save experiment', err); }
3051
+ }
3052
+
3053
+ // Poll session stats every 2 seconds
3054
+ sessionPollInterval = setInterval(pollSessionStats, 2000);
3055
+ pollSessionStats();
3056
+
3057
+ // ============================================
3058
+ // BOOT
3059
+ // ============================================
3060
+ init();
3061
+ <\/script>
3062
+ </body>
3063
+ </html>`;