@neuroverseos/nv-sim 0.1.2 → 0.1.5

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 (53) hide show
  1. package/README.md +562 -68
  2. package/dist/adapters/mirofish.js +461 -0
  3. package/dist/adapters/scienceclaw.js +750 -0
  4. package/dist/assets/index-B64NuIXu.css +1 -0
  5. package/dist/assets/index-DbzSnYxr.js +532 -0
  6. package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
  7. package/dist/assets/reportEngine-DKWTrP6-.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 +4284 -0
  27. package/dist/engine/metrics/science.metrics.js +335 -0
  28. package/dist/engine/narrativeInjection.js +360 -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/scenarioCapsule.js +56 -0
  35. package/dist/engine/scenarioComparison.js +463 -0
  36. package/dist/engine/scenarioLibrary.js +248 -0
  37. package/dist/engine/swarmSimulation.js +54 -1
  38. package/dist/engine/worldComparison.js +358 -0
  39. package/dist/engine/worldStorage.js +232 -0
  40. package/dist/favicon.ico +0 -0
  41. package/dist/index.html +23 -0
  42. package/dist/lib/reasoningEngine.js +290 -0
  43. package/dist/lib/simulationAdapter.js +686 -0
  44. package/dist/lib/swarmParser.js +291 -0
  45. package/dist/lib/types.js +2 -0
  46. package/dist/lib/utils.js +8 -0
  47. package/dist/placeholder.svg +1 -0
  48. package/dist/robots.txt +14 -0
  49. package/dist/runtime/govern.js +473 -0
  50. package/dist/runtime/index.js +75 -0
  51. package/dist/runtime/types.js +11 -0
  52. package/package.json +17 -12
  53. package/variants/.gitkeep +0 -0
@@ -0,0 +1,4284 @@
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
+ // ── Bridge Metrics Tracker ──
190
+ // Computes meaningful live metrics from external /api/evaluate calls
191
+ let bridgeEvalCount = 0;
192
+ let bridgeBlockCount = 0;
193
+ let bridgeModifyCount = 0;
194
+ function computeBridgeMetrics(decision) {
195
+ bridgeEvalCount++;
196
+ if (decision === "BLOCK")
197
+ bridgeBlockCount++;
198
+ if (decision === "MODIFY")
199
+ bridgeModifyCount++;
200
+ const totalInterventions = bridgeBlockCount + bridgeModifyCount;
201
+ // Stability = ratio of non-blocked actions (higher = more stable)
202
+ const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount) / bridgeEvalCount : 1;
203
+ // Volatility = ratio of interventions (blocks + modifies) — more interventions = more volatile
204
+ const volatility = bridgeEvalCount > 0 ? totalInterventions / bridgeEvalCount : 0;
205
+ return { stability, volatility, totalInterventions, evalCount: bridgeEvalCount };
206
+ }
207
+ function synthesizeSessionReport() {
208
+ // Gather all sessions to report on (history + current if it has data)
209
+ const allSessions = [
210
+ ...sessionHistory,
211
+ ...(currentSession.evaluations.length > 0 ? [currentSession] : []),
212
+ ];
213
+ if (allSessions.length === 0) {
214
+ return {
215
+ sessionId: currentSession.id,
216
+ scenario: "Live governance session (no evaluations yet)",
217
+ runs: [],
218
+ divergence: {
219
+ stabilityTrend: [],
220
+ collapseTrend: [],
221
+ effectivenessTrend: [],
222
+ bestIteration: 0,
223
+ worstIteration: 0,
224
+ totalDivergence: 0,
225
+ narrative: "No evaluations recorded. Send actions to POST /api/evaluate to begin.",
226
+ },
227
+ recommendation: "Start sending agent actions to /api/evaluate to generate governance data.",
228
+ generatedAt: new Date().toISOString(),
229
+ };
230
+ }
231
+ const runs = allSessions.map((sess, idx) => {
232
+ const evals = sess.evaluations;
233
+ const total = evals.length;
234
+ const blocked = evals.filter(e => e.decision === "BLOCK").length;
235
+ const modified = evals.filter(e => e.decision === "MODIFY").length;
236
+ const allowed = evals.filter(e => e.decision === "ALLOW").length;
237
+ // Synthesize metrics from real evaluation data
238
+ const blockRate = total > 0 ? blocked / total : 0;
239
+ const interventionRate = total > 0 ? (blocked + modified) / total : 0;
240
+ // Stability: higher when governance is actively catching harmful actions
241
+ // If nothing is blocked, either the system is clean OR governance is too weak
242
+ const stabilityScore = total > 0
243
+ ? Math.min(0.95, 0.4 + interventionRate * 0.4 + (allowed / Math.max(1, total)) * 0.2)
244
+ : 0.5;
245
+ // Collapse probability: lower when more harmful actions are caught
246
+ const collapseProbability = total > 0
247
+ ? Math.max(0.02, 0.6 - interventionRate * 0.5 - blockRate * 0.15)
248
+ : 0.5;
249
+ // Governance effectiveness: composite of intervention quality
250
+ const governanceEffectiveness = total > 0
251
+ ? Math.min(0.95, interventionRate * 0.6 + blockRate * 0.3 + (total > 10 ? 0.1 : 0))
252
+ : 0;
253
+ // Unique rules that fired
254
+ const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
255
+ // Unique actors and actions
256
+ const uniqueActors = [...new Set(evals.map(e => e.actor))];
257
+ const uniqueActions = [...new Set(evals.map(e => e.action))];
258
+ return {
259
+ iteration: idx + 1,
260
+ worldName: sess.world || "live-session",
261
+ ruleCount: sess.guardCount,
262
+ gateCount: 0,
263
+ metrics: {
264
+ avgImpact: interventionRate > 0 ? -(interventionRate * 0.5) : 0.1,
265
+ collapseProbability,
266
+ stabilityScore,
267
+ coalitionRisks: 0,
268
+ polarizationEvents: 0,
269
+ peakNegativeSentiment: blockRate > 0.3 ? -0.6 : -0.2,
270
+ consensusRounds: 0,
271
+ maxVolatility: blockRate > 0.2 ? 0.7 : 0.3,
272
+ },
273
+ comparison: {
274
+ collapseReduction: collapseProbability < 0.5 ? (0.6 - collapseProbability) : 0,
275
+ stabilityImprovement: stabilityScore > 0.5 ? (stabilityScore - 0.4) : 0,
276
+ volatilityReduction: interventionRate * 0.5,
277
+ coalitionRiskReduction: 0,
278
+ governanceEffectiveness,
279
+ narrative: total > 0
280
+ ? `${total} actions evaluated: ${blocked} blocked, ${modified} modified, ${allowed} allowed across ${uniqueActors.length} agent(s) performing ${uniqueActions.length} action type(s).`
281
+ : "No evaluations in this session.",
282
+ },
283
+ governanceStats: {
284
+ engineLoaded: true,
285
+ totalEvaluations: total,
286
+ verdicts: { allow: allowed, block: blocked, pause: modified },
287
+ rulesFired: triggeredRules.length,
288
+ worldCollapsed: false,
289
+ finalViability: stabilityScore > 0.6 ? "stable" : stabilityScore > 0.4 ? "at-risk" : "critical",
290
+ invariantsChecked: sess.guardCount,
291
+ triggeredGuards: triggeredRules,
292
+ },
293
+ };
294
+ });
295
+ // Compute rule changes between sessions
296
+ for (let i = 1; i < runs.length; i++) {
297
+ const prev = allSessions[i - 1];
298
+ const curr = allSessions[i];
299
+ if (prev.guardCount !== curr.guardCount) {
300
+ runs[i].ruleChanges = {
301
+ added: [],
302
+ removed: [],
303
+ gatesAdded: [],
304
+ gatesRemoved: [],
305
+ thesisChanged: prev.world !== curr.world,
306
+ };
307
+ }
308
+ }
309
+ // Divergence analysis
310
+ const stabilityTrend = runs.map(r => r.metrics.stabilityScore);
311
+ const collapseTrend = runs.map(r => r.metrics.collapseProbability);
312
+ const effectivenessTrend = runs.map(r => r.comparison.governanceEffectiveness);
313
+ let bestIteration = 1;
314
+ let worstIteration = 1;
315
+ let bestScore = -Infinity;
316
+ let worstScore = Infinity;
317
+ for (const run of runs) {
318
+ if (run.comparison.governanceEffectiveness > bestScore) {
319
+ bestScore = run.comparison.governanceEffectiveness;
320
+ bestIteration = run.iteration;
321
+ }
322
+ if (run.comparison.governanceEffectiveness < worstScore) {
323
+ worstScore = run.comparison.governanceEffectiveness;
324
+ worstIteration = run.iteration;
325
+ }
326
+ }
327
+ let totalDivergence = 0;
328
+ for (let i = 1; i < runs.length; i++) {
329
+ totalDivergence += Math.abs(runs[i].metrics.stabilityScore - runs[i - 1].metrics.stabilityScore);
330
+ totalDivergence += Math.abs(runs[i].metrics.collapseProbability - runs[i - 1].metrics.collapseProbability);
331
+ totalDivergence += Math.abs(runs[i].comparison.governanceEffectiveness - runs[i - 1].comparison.governanceEffectiveness);
332
+ }
333
+ // Build narrative
334
+ const totalEvals = allSessions.reduce((sum, s) => sum + s.evaluations.length, 0);
335
+ const totalBlocked = allSessions.reduce((sum, s) => sum + s.evaluations.filter(e => e.decision === "BLOCK").length, 0);
336
+ const narrativeParts = [];
337
+ narrativeParts.push(`${allSessions.length} session(s), ${totalEvals} total evaluations, ${totalBlocked} blocked.`);
338
+ if (runs.length > 1) {
339
+ const stabDelta = stabilityTrend[stabilityTrend.length - 1] - stabilityTrend[0];
340
+ if (stabDelta > 0.05)
341
+ narrativeParts.push(`Stability improved ${(stabDelta * 100).toFixed(0)}pp across sessions.`);
342
+ narrativeParts.push(`Best outcome: session ${bestIteration}.`);
343
+ }
344
+ const best = runs[bestIteration - 1];
345
+ const recommendation = runs.length === 1
346
+ ? `Single session: ${best.governanceStats.totalEvaluations} evaluations, ${best.governanceStats.verdicts.block} blocked. Apply different rules and reset to compare.`
347
+ : `Best session: #${bestIteration} ("${best.worldName}") — ${(best.comparison.governanceEffectiveness * 100).toFixed(0)}% effectiveness, ${(best.metrics.stabilityScore * 100).toFixed(0)}% stability.`;
348
+ return {
349
+ sessionId: currentSession.id,
350
+ scenario: `Live governance — ${currentSession.world}`,
351
+ runs,
352
+ divergence: {
353
+ stabilityTrend,
354
+ collapseTrend,
355
+ effectivenessTrend,
356
+ bestIteration,
357
+ worstIteration,
358
+ totalDivergence: Number(totalDivergence.toFixed(3)),
359
+ narrative: narrativeParts.join(" "),
360
+ },
361
+ recommendation,
362
+ generatedAt: new Date().toISOString(),
363
+ };
364
+ }
365
+ function broadcast(event) {
366
+ const data = `data: ${JSON.stringify(event)}\n\n`;
367
+ for (const client of clients) {
368
+ client.write(data);
369
+ }
370
+ }
371
+ function readBody(req) {
372
+ return new Promise((resolve) => {
373
+ let body = "";
374
+ req.on("data", (chunk) => { body += chunk.toString(); });
375
+ req.on("end", () => resolve(body));
376
+ });
377
+ }
378
+ function jsonResponse(res, status, data) {
379
+ res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
380
+ res.end(JSON.stringify(data));
381
+ }
382
+ async function runSimulation(config) {
383
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
384
+ const { runGovernedComparison } = await Promise.resolve().then(() => __importStar(require("./governedSimulation")));
385
+ const resolved = resolveWorld(config.worldId);
386
+ const rounds = config.rounds ?? resolved.swarm.rounds ?? 5;
387
+ // Apply state variable overrides to world definition
388
+ const world = { ...resolved.world };
389
+ if (config.stateOverrides) {
390
+ const updatedVars = world.state_variables.map(sv => {
391
+ if (config.stateOverrides && sv.id in config.stateOverrides) {
392
+ return { ...sv, default_value: config.stateOverrides[sv.id] };
393
+ }
394
+ return sv;
395
+ });
396
+ world.state_variables = updatedVars;
397
+ }
398
+ // Inject custom rules as first-class invariants (not just guards)
399
+ // This ensures user rules affect BOTH governance paths:
400
+ // 1. Guard engine (intent-level blocking) — already wired via customGuards[]
401
+ // 2. Invariant engine (system-level stability shaping) — wired here
402
+ if (customGuards.length > 0) {
403
+ const customInvariants = customGuards.map(cg => ({
404
+ id: cg.id,
405
+ description: cg.description,
406
+ enforceable: cg.enforcement === "block",
407
+ }));
408
+ world.invariants = [...world.invariants, ...customInvariants];
409
+ }
410
+ // Resolve narrative events
411
+ let narrativeEvents = [];
412
+ if (config.scenarioId && scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]) {
413
+ narrativeEvents = (0, scenarioLibrary_1.resolveScenarioEvents)(scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]);
414
+ }
415
+ else if (config.injectEvents && config.injectEvents.length > 0) {
416
+ narrativeEvents = (0, narrativeInjection_1.parseInjectArgs)(["--inject", config.injectEvents.join(",")]);
417
+ }
418
+ const request = {
419
+ scenario: resolved.scenario,
420
+ stakeholders: resolved.stakeholders,
421
+ assumptions: resolved.assumptions,
422
+ constraints: resolved.constraints,
423
+ depth: resolved.depth,
424
+ swarm: { ...resolved.swarm, rounds },
425
+ };
426
+ // Send meta event
427
+ broadcast({
428
+ type: "meta",
429
+ source: "nv-sim",
430
+ scenario: resolved.scenario,
431
+ worldThesis: world.thesis,
432
+ agents: resolved.stakeholders.map(s => s.id),
433
+ invariants: world.invariants.map(inv => ({
434
+ id: inv.id,
435
+ description: inv.description,
436
+ enforcement: inv.enforceable ? "full" : "advisory",
437
+ source: inv.id.startsWith("custom-rule-") ? "user" : "world",
438
+ })),
439
+ gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
440
+ narrativeEvents: narrativeEvents.map(e => ({ id: e.id, headline: e.headline, round: e.round, severity: e.severity })),
441
+ totalRounds: rounds,
442
+ });
443
+ // Run baseline
444
+ const baselineResult = await (0, swarmSimulation_1.runSwarmSimulation)(resolved.scenario, resolved.stakeholders, resolved.paths, { ...resolved.swarm, rounds });
445
+ for (const round of baselineResult.rounds) {
446
+ broadcast({
447
+ type: "round",
448
+ round: round.round,
449
+ totalRounds: rounds,
450
+ phase: "baseline",
451
+ reactions: round.reactions.map(r => ({
452
+ stakeholder_id: r.stakeholder_id,
453
+ reaction: r.reaction,
454
+ impact: r.impact,
455
+ confidence: r.confidence,
456
+ trigger: r.trigger,
457
+ })),
458
+ avgImpact: round.reactions.reduce((s, r) => s + r.impact, 0) / round.reactions.length,
459
+ maxVolatility: Math.max(...round.reactions.map(r => Math.abs(r.impact))),
460
+ dynamics: round.emergent_dynamics ?? [],
461
+ interventionCount: 0,
462
+ });
463
+ await new Promise(r => setTimeout(r, 400));
464
+ }
465
+ // Run governed with narrative events
466
+ const governedResult = await runGovernedComparison(request, world, resolved.paths, narrativeEvents);
467
+ const nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, world);
468
+ for (const round of governedResult.governed.swarm.rounds) {
469
+ const reactions = round.reactions.map(r => {
470
+ const verdict = (0, worldBridge_1.evaluateScenarioGuard)({ ...request, scenario: `[R${round.round}] ${r.stakeholder_id}: ${r.reaction}` }, nvWorld, { trace: true, level: "standard" });
471
+ return {
472
+ stakeholder_id: r.stakeholder_id,
473
+ reaction: r.reaction,
474
+ impact: r.impact,
475
+ confidence: r.confidence,
476
+ trigger: r.trigger,
477
+ verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
478
+ };
479
+ });
480
+ const interventionCount = (round.emergent_dynamics ?? []).filter(d => d.includes("intervention") || d.includes("governance")).length;
481
+ broadcast({
482
+ type: "round",
483
+ round: round.round,
484
+ totalRounds: rounds,
485
+ phase: "governed",
486
+ reactions,
487
+ avgImpact: round.reactions.reduce((s, r) => s + r.impact, 0) / round.reactions.length,
488
+ maxVolatility: Math.max(...round.reactions.map(r => Math.abs(r.impact))),
489
+ dynamics: round.emergent_dynamics ?? [],
490
+ interventionCount,
491
+ });
492
+ await new Promise(r => setTimeout(r, 500));
493
+ }
494
+ broadcast({ type: "complete", result: governedResult });
495
+ }
496
+ const server = http.createServer(async (req, res) => {
497
+ // CORS preflight
498
+ if (req.method === "OPTIONS") {
499
+ res.writeHead(204, {
500
+ "Access-Control-Allow-Origin": "*",
501
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
502
+ "Access-Control-Allow-Headers": "Content-Type",
503
+ });
504
+ res.end();
505
+ return;
506
+ }
507
+ if (req.url === "/events") {
508
+ res.writeHead(200, {
509
+ "Content-Type": "text/event-stream",
510
+ "Cache-Control": "no-cache",
511
+ "Connection": "keep-alive",
512
+ "Access-Control-Allow-Origin": "*",
513
+ });
514
+ clients.add(res);
515
+ req.on("close", () => clients.delete(res));
516
+ return;
517
+ }
518
+ if (req.url === "/api/worlds" && req.method === "GET") {
519
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
520
+ const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis", "science_research"];
521
+ const worlds = worldIds.map(id => {
522
+ try {
523
+ const r = resolveWorld(id);
524
+ return {
525
+ id,
526
+ title: r.title,
527
+ thesis: r.world.thesis,
528
+ stateVariables: r.world.state_variables,
529
+ invariants: r.world.invariants,
530
+ gates: r.world.gates ?? [],
531
+ };
532
+ }
533
+ catch {
534
+ return null;
535
+ }
536
+ }).filter(Boolean);
537
+ jsonResponse(res, 200, { worlds });
538
+ return;
539
+ }
540
+ if (req.url === "/api/scenarios" && req.method === "GET") {
541
+ jsonResponse(res, 200, { scenarios: scenarioLibrary_1.SCENARIO_LIBRARY });
542
+ return;
543
+ }
544
+ if (req.url === "/api/narratives" && req.method === "GET") {
545
+ jsonResponse(res, 200, { narratives: narrativeInjection_1.NARRATIVE_PRESETS });
546
+ return;
547
+ }
548
+ if (req.url === "/api/variants" && req.method === "GET") {
549
+ const variants = loadAllVariants();
550
+ jsonResponse(res, 200, { variants });
551
+ return;
552
+ }
553
+ if (req.url === "/api/save-variant" && req.method === "POST") {
554
+ try {
555
+ const body = await readBody(req);
556
+ const payload = JSON.parse(body);
557
+ if (!payload.name || !payload.baseWorld) {
558
+ jsonResponse(res, 400, { error: "name and baseWorld are required" });
559
+ return;
560
+ }
561
+ const id = slugify(payload.name);
562
+ const variant = {
563
+ id,
564
+ name: payload.name,
565
+ description: payload.description ?? "",
566
+ baseWorld: payload.baseWorld,
567
+ stateOverrides: payload.stateOverrides ?? {},
568
+ events: payload.events ?? [],
569
+ rounds: payload.rounds ?? 5,
570
+ createdAt: new Date().toISOString(),
571
+ lastResult: payload.lastResult,
572
+ };
573
+ const filepath = saveVariant(variant);
574
+ jsonResponse(res, 200, { status: "saved", variant, filepath });
575
+ }
576
+ catch (err) {
577
+ jsonResponse(res, 400, { error: "Invalid request body" });
578
+ }
579
+ return;
580
+ }
581
+ if (req.url?.startsWith("/api/delete-variant/") && req.method === "DELETE") {
582
+ const variantId = req.url.split("/api/delete-variant/")[1];
583
+ if (variantId && deleteVariant(variantId)) {
584
+ jsonResponse(res, 200, { status: "deleted", id: variantId });
585
+ }
586
+ else {
587
+ jsonResponse(res, 404, { error: "Variant not found" });
588
+ }
589
+ return;
590
+ }
591
+ if (req.url === "/api/run-sim" && req.method === "POST") {
592
+ if (isRunning) {
593
+ jsonResponse(res, 409, { error: "Simulation already running" });
594
+ return;
595
+ }
596
+ try {
597
+ const body = await readBody(req);
598
+ const config = JSON.parse(body);
599
+ isRunning = true;
600
+ jsonResponse(res, 200, { status: "started", config });
601
+ // Run simulation async, streaming via SSE
602
+ runSimulation(config).catch(err => {
603
+ broadcast({ type: "complete", result: { error: String(err) } });
604
+ }).finally(() => { isRunning = false; });
605
+ }
606
+ catch (err) {
607
+ jsonResponse(res, 400, { error: "Invalid request body" });
608
+ }
609
+ return;
610
+ }
611
+ // ── Governance Evaluate Endpoint ──
612
+ // Universal bridge endpoint: external simulators POST actions here for governance evaluation.
613
+ // Contract: { actor, action, payload, state? } → { decision: ALLOW|BLOCK|MODIFY, reason, modified_action? }
614
+ if (req.url === "/api/evaluate" && req.method === "POST") {
615
+ try {
616
+ const body = await readBody(req);
617
+ const payload = JSON.parse(body);
618
+ if (!payload.actor || !payload.action) {
619
+ jsonResponse(res, 400, { error: "actor and action are required" });
620
+ return;
621
+ }
622
+ // Resolve world for evaluation
623
+ const worldId = payload.world ?? "trading";
624
+ let nvWorld;
625
+ try {
626
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
627
+ const resolved = resolveWorld(worldId);
628
+ const request = {
629
+ scenario: resolved.scenario,
630
+ stakeholders: resolved.stakeholders,
631
+ assumptions: resolved.assumptions,
632
+ constraints: resolved.constraints,
633
+ depth: resolved.depth,
634
+ swarm: resolved.swarm,
635
+ };
636
+ nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, resolved.world);
637
+ }
638
+ catch {
639
+ // Fallback: build minimal world
640
+ nvWorld = (0, worldBridge_1.buildWorldFromScenario)({
641
+ scenario: `${payload.actor}: ${payload.action}`,
642
+ stakeholders: [],
643
+ });
644
+ }
645
+ // Enable action_space — bridge actions are simulation execution, not thinking-only.
646
+ // Without this, the safety layer flags normal actions like "buy" as execution intents
647
+ // in a thinking-only environment.
648
+ nvWorld.world.players = { ...nvWorld.world.players, action_space: true };
649
+ // Inject simulation-specific guards into the world.
650
+ // The guard engine pattern-matches event.intent against guard.intent_patterns.
651
+ // Without guards, everything defaults to ALLOW. These guards define what
652
+ // simulation actions should be governed.
653
+ if (!nvWorld.guards) {
654
+ // Detect world type from request — social media worlds get social guards,
655
+ // financial worlds get trading guards, unknown gets both.
656
+ const isSocial = payload.world === "social-media" || payload.world === "social"
657
+ || ["create_post", "like_post", "repost", "follow", "unfollow", "create_comment",
658
+ "search_posts", "mute", "unmute", "trend"].includes(payload.action);
659
+ const financialGuards = [
660
+ {
661
+ id: "sim-panic-actions",
662
+ label: "Block panic-driven actions",
663
+ description: "Prevents panic selling, aggressive shorting, and other destabilizing actions during high volatility",
664
+ category: "structural",
665
+ enforcement: "block",
666
+ immutable: true,
667
+ invariant_ref: nvWorld.invariants[0]?.id,
668
+ intent_patterns: ["panic_sell", "panic sell", "panic buy", "panic_buy"],
669
+ default_enabled: true,
670
+ },
671
+ {
672
+ id: "sim-excessive-leverage",
673
+ label: "Block excessive leverage",
674
+ description: "Prevents increasing leverage positions that could amplify cascades",
675
+ category: "structural",
676
+ enforcement: "block",
677
+ immutable: true,
678
+ intent_patterns: ["increase_leverage", "increase leverage", "max leverage"],
679
+ default_enabled: true,
680
+ },
681
+ {
682
+ id: "sim-aggressive-actions",
683
+ label: "Pause aggressive market actions",
684
+ description: "Requires review for aggressive buying or shorting that could move markets",
685
+ category: "operational",
686
+ enforcement: "pause",
687
+ immutable: false,
688
+ intent_patterns: ["aggressive_buy", "aggressive buy", "aggressive_short", "short"],
689
+ default_enabled: true,
690
+ },
691
+ ];
692
+ // Social media guards for MiroFish/OASIS agent actions.
693
+ // These govern what AI agents can do on simulated social platforms.
694
+ const socialGuards = [
695
+ {
696
+ id: "social-harmful-content",
697
+ label: "Block harmful content creation",
698
+ description: "Prevents agents from posting content that incites panic, spreads disinformation, or promotes harmful behavior",
699
+ category: "structural",
700
+ enforcement: "block",
701
+ immutable: true,
702
+ intent_patterns: ["create_post", "create_comment", "quote_post"],
703
+ content_patterns: ["panic", "crash", "collaps", "sell everything", "market is dead", "scam", "rug pull", "ponzi"],
704
+ default_enabled: true,
705
+ },
706
+ {
707
+ id: "social-coordinated-manipulation",
708
+ label: "Block coordinated manipulation",
709
+ description: "Prevents agents from engaging in coordinated inauthentic behavior like mass following, mass liking, or brigading",
710
+ category: "structural",
711
+ enforcement: "block",
712
+ immutable: true,
713
+ intent_patterns: ["follow", "like_post", "repost"],
714
+ default_enabled: true,
715
+ },
716
+ {
717
+ id: "social-spam-prevention",
718
+ label: "Pause high-frequency posting",
719
+ description: "Rate-limits agents that post too frequently, preventing spam and platform flooding",
720
+ category: "operational",
721
+ enforcement: "pause",
722
+ immutable: false,
723
+ intent_patterns: ["create_post", "create_comment", "repost", "quote_post"],
724
+ default_enabled: true,
725
+ },
726
+ {
727
+ id: "social-engagement-farming",
728
+ label: "Block engagement farming",
729
+ description: "Prevents agents from like-bombing, follow-unfollowing, or other engagement manipulation tactics",
730
+ category: "operational",
731
+ enforcement: "block",
732
+ immutable: false,
733
+ intent_patterns: ["like_post", "unlike_post", "follow", "unfollow"],
734
+ default_enabled: true,
735
+ },
736
+ ];
737
+ const guards = isSocial ? socialGuards : financialGuards;
738
+ // Build intent vocabulary from selected guards
739
+ const intentVocabulary = {};
740
+ for (const g of guards) {
741
+ for (const pat of g.intent_patterns) {
742
+ if (!intentVocabulary[pat]) {
743
+ intentVocabulary[pat] = { label: pat.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()), pattern: pat };
744
+ }
745
+ }
746
+ }
747
+ nvWorld.guards = { guards, intent_vocabulary: intentVocabulary };
748
+ }
749
+ // Inject custom guards from plain-English rule editor
750
+ if (customGuards.length > 0 && nvWorld.guards) {
751
+ const existingGuards = nvWorld.guards.guards;
752
+ for (const cg of customGuards) {
753
+ existingGuards.push({
754
+ id: cg.id,
755
+ label: cg.label,
756
+ description: cg.description,
757
+ category: cg.category,
758
+ enforcement: cg.enforcement,
759
+ immutable: false,
760
+ intent_patterns: cg.intent_patterns,
761
+ default_enabled: true,
762
+ });
763
+ // Add patterns to vocabulary
764
+ for (const pat of cg.intent_patterns) {
765
+ nvWorld.guards.intent_vocabulary[pat] = { label: pat, pattern: pat };
766
+ }
767
+ }
768
+ }
769
+ // Content-aware governance for social media actions.
770
+ // The guard engine matches intent against intent_patterns, but for social media
771
+ // we also need to check message content for harmful patterns.
772
+ const messageContent = typeof payload.payload?.message === "string"
773
+ ? payload.payload.message.toLowerCase()
774
+ : typeof payload.payload?.content === "string"
775
+ ? payload.payload.content.toLowerCase()
776
+ : "";
777
+ const contentPatterns = ["panic", "crash", "collaps", "sell everything", "market is dead",
778
+ "scam", "rug pull", "ponzi", "buy now or lose", "guaranteed returns", "get rich"];
779
+ const hasHarmfulContent = messageContent.length > 0
780
+ && contentPatterns.some(p => messageContent.includes(p));
781
+ // If content is harmful and action is content-creating, short-circuit to BLOCK
782
+ const isContentAction = ["create_post", "create_comment", "quote_post", "repost"].includes(payload.action);
783
+ if (hasHarmfulContent && isContentAction) {
784
+ const matchedPattern = contentPatterns.find(p => messageContent.includes(p)) ?? "harmful content";
785
+ broadcast({
786
+ type: "round",
787
+ round: 0,
788
+ totalRounds: 0,
789
+ phase: "governed",
790
+ reactions: [{
791
+ stakeholder_id: payload.actor,
792
+ reaction: payload.action,
793
+ impact: 0,
794
+ confidence: 0.9,
795
+ trigger: "bridge",
796
+ verdict: { status: "BLOCK", reason: `Content violates governance: "${matchedPattern}" detected`, ruleId: "social-harmful-content" },
797
+ }],
798
+ avgImpact: 0,
799
+ maxVolatility: 0,
800
+ dynamics: [],
801
+ interventionCount: 1,
802
+ });
803
+ // Record in session
804
+ currentSession.evaluations.push({
805
+ actor: payload.actor, action: payload.action, decision: "BLOCK",
806
+ reason: `Content blocked: "${matchedPattern}" detected`,
807
+ ruleId: "social-harmful-content", world: payload.world ?? currentSession.world,
808
+ timestamp: Date.now(), payload: payload.payload,
809
+ });
810
+ jsonResponse(res, 200, {
811
+ decision: "BLOCK",
812
+ reason: `Content blocked: "${matchedPattern}" detected in ${payload.action} — violates social media governance policy`,
813
+ rule_id: "social-harmful-content",
814
+ evidence: { matched_pattern: matchedPattern, action: payload.action, actor: payload.actor },
815
+ modified_action: null,
816
+ });
817
+ return;
818
+ }
819
+ // Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
820
+ // Omit `direction` — setting it enables execution-intent safety checks (prompt injection
821
+ // detection) which falsely flag financial terms like "buy" and "sell". Bridge actions are
822
+ // simulation commands, not user prompts.
823
+ const guardEvent = {
824
+ intent: payload.action,
825
+ tool: "simulation",
826
+ scope: `bridge/${payload.actor}`,
827
+ actionCategory: "execute",
828
+ riskLevel: (["panic_sell", "panic_buy", "increase_leverage", "create_post", "repost", "quote_post"].includes(payload.action) ? "high" : "medium"),
829
+ args: {
830
+ actor: payload.actor,
831
+ action: payload.action,
832
+ ...(payload.payload ?? {}),
833
+ },
834
+ };
835
+ // Evaluate directly via the governance module
836
+ let verdict;
837
+ try {
838
+ const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
839
+ verdict = nv.evaluateGuard(guardEvent, nvWorld, {
840
+ trace: true,
841
+ level: "standard",
842
+ });
843
+ }
844
+ catch {
845
+ // Fallback to scenario guard evaluation
846
+ verdict = (0, worldBridge_1.evaluateScenarioGuard)({
847
+ scenario: `[BRIDGE] ${payload.actor}: ${payload.action}`,
848
+ stakeholders: [{ id: payload.actor, description: payload.actor, disposition: "neutral", priorities: [] }],
849
+ }, nvWorld, { trace: true, level: "standard" });
850
+ }
851
+ // Map verdict to bridge protocol
852
+ const decision = verdict.status === "BLOCK" ? "BLOCK"
853
+ : verdict.status === "PAUSE" ? "MODIFY"
854
+ : "ALLOW";
855
+ // Compute live metrics from cumulative bridge evaluations
856
+ const bridgeMetrics = computeBridgeMetrics(decision);
857
+ // Broadcast governance event to connected SSE clients
858
+ broadcast({
859
+ type: "round",
860
+ round: bridgeMetrics.evalCount,
861
+ totalRounds: 0,
862
+ phase: "governed",
863
+ reactions: [{
864
+ stakeholder_id: payload.actor,
865
+ reaction: payload.action,
866
+ impact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
867
+ confidence: 0.5,
868
+ trigger: "bridge",
869
+ verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
870
+ }],
871
+ avgImpact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
872
+ maxVolatility: bridgeMetrics.volatility,
873
+ dynamics: [],
874
+ interventionCount: decision !== "ALLOW" ? 1 : 0,
875
+ });
876
+ // Also broadcast a stability update so the metric refreshes live
877
+ broadcast({
878
+ type: "bridge_metrics",
879
+ stability: bridgeMetrics.stability,
880
+ volatility: bridgeMetrics.volatility,
881
+ evalCount: bridgeMetrics.evalCount,
882
+ totalInterventions: bridgeMetrics.totalInterventions,
883
+ });
884
+ // Record in session
885
+ currentSession.evaluations.push({
886
+ actor: payload.actor, action: payload.action,
887
+ decision: decision,
888
+ reason: verdict.reason ?? "", ruleId: verdict.ruleId ?? null,
889
+ world: payload.world ?? currentSession.world,
890
+ timestamp: Date.now(), payload: payload.payload,
891
+ });
892
+ currentSession.guardCount = customGuards.length + (nvWorld.guards?.guards?.length ?? 0);
893
+ // Persist to audit trail on disk
894
+ auditTrail.logVerdict({
895
+ agent: payload.actor,
896
+ action: payload.action,
897
+ actionType: payload.payload?.type ?? "unknown",
898
+ verdict: decision,
899
+ reason: verdict.reason ?? "",
900
+ confidence: verdict.confidence ?? 0.5,
901
+ rulesFired: verdict.ruleId ? [{
902
+ id: verdict.ruleId,
903
+ description: verdict.reason ?? "",
904
+ effect: decision === "BLOCK" ? "blocked" : decision === "MODIFY" ? "dampened" : "monitored",
905
+ impactReduction: decision === "BLOCK" ? 1 : decision === "MODIFY" ? 0.5 : 0,
906
+ }] : [],
907
+ worldState: payload.world ?? currentSession.world,
908
+ });
909
+ jsonResponse(res, 200, {
910
+ decision,
911
+ reason: verdict.reason ?? null,
912
+ rule_id: verdict.ruleId ?? null,
913
+ evidence: verdict.evidence ?? null,
914
+ modified_action: decision === "MODIFY" ? payload.payload : null,
915
+ });
916
+ }
917
+ catch (err) {
918
+ // Record fail-open in session (payload may be out of scope if JSON parse failed)
919
+ currentSession.evaluations.push({
920
+ actor: "unknown", action: "unknown",
921
+ decision: "ALLOW", reason: "Governance evaluation error — fail open",
922
+ ruleId: null, world: currentSession.world, timestamp: Date.now(),
923
+ });
924
+ // Fail open — return ALLOW on any error
925
+ jsonResponse(res, 200, {
926
+ decision: "ALLOW",
927
+ reason: "Governance evaluation error — fail open",
928
+ rule_id: null,
929
+ evidence: null,
930
+ modified_action: null,
931
+ });
932
+ }
933
+ return;
934
+ }
935
+ // ── Plain-English Rule Parser ──
936
+ // Parses natural language rules into guard definitions (local, no LLM needed)
937
+ if (req.url === "/api/parse-rules" && req.method === "POST") {
938
+ try {
939
+ const body = await readBody(req);
940
+ const payload = JSON.parse(body);
941
+ if (!payload.text) {
942
+ jsonResponse(res, 400, { error: "text is required" });
943
+ return;
944
+ }
945
+ const lines = payload.text.split("\n").map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith("#"));
946
+ const rules = lines.map((line, i) => parseNaturalLanguageRule(line, i));
947
+ jsonResponse(res, 200, { rules: rules.filter(Boolean), raw: lines });
948
+ }
949
+ catch (err) {
950
+ jsonResponse(res, 400, { error: "Invalid request" });
951
+ }
952
+ return;
953
+ }
954
+ // Apply parsed rules to the active governance context
955
+ if (req.url === "/api/apply-rules" && req.method === "POST") {
956
+ try {
957
+ const body = await readBody(req);
958
+ const payload = JSON.parse(body);
959
+ if (!payload.rules || !Array.isArray(payload.rules)) {
960
+ jsonResponse(res, 400, { error: "rules array is required" });
961
+ return;
962
+ }
963
+ // Store custom rules in memory for the session
964
+ customGuards.length = 0;
965
+ for (const rule of payload.rules) {
966
+ customGuards.push({
967
+ id: rule.id,
968
+ label: rule.description,
969
+ description: rule.description,
970
+ category: "custom",
971
+ enforcement: rule.enforcement,
972
+ immutable: false,
973
+ intent_patterns: rule.intent_patterns,
974
+ default_enabled: true,
975
+ });
976
+ }
977
+ jsonResponse(res, 200, {
978
+ status: "applied",
979
+ applied: customGuards.length,
980
+ enforcement: "full",
981
+ detail: "Rules enforced across guard engine (intent blocking) AND invariant engine (system dynamics)",
982
+ });
983
+ }
984
+ catch (err) {
985
+ jsonResponse(res, 400, { error: "Invalid request" });
986
+ }
987
+ return;
988
+ }
989
+ // ── Clear Rules ──
990
+ // Wipes all custom guards and resets governance to base world rules
991
+ if (req.url === "/api/clear-rules" && req.method === "POST") {
992
+ customGuards.length = 0;
993
+ jsonResponse(res, 200, {
994
+ status: "cleared",
995
+ message: "All custom rules removed. Governance reset to base world rules.",
996
+ });
997
+ return;
998
+ }
999
+ // ── Load World File ──
1000
+ // Accept a full world definition JSON and use it as the active world
1001
+ if (req.url === "/api/load-world-file" && req.method === "POST") {
1002
+ try {
1003
+ const body = await readBody(req);
1004
+ const payload = JSON.parse(body);
1005
+ if (!payload.world) {
1006
+ jsonResponse(res, 400, { error: "world object is required" });
1007
+ return;
1008
+ }
1009
+ const w = payload.world;
1010
+ // Parse any plain-English rules in the world file into guards
1011
+ customGuards.length = 0;
1012
+ if (w.rules && Array.isArray(w.rules)) {
1013
+ for (let i = 0; i < w.rules.length; i++) {
1014
+ const rule = w.rules[i];
1015
+ if (rule.intent_patterns && rule.intent_patterns.length > 0) {
1016
+ // Already structured rule
1017
+ customGuards.push({
1018
+ id: rule.id || `world-rule-${i}`,
1019
+ label: rule.description,
1020
+ description: rule.description,
1021
+ category: "world-file",
1022
+ enforcement: rule.enforcement || "block",
1023
+ immutable: false,
1024
+ intent_patterns: rule.intent_patterns,
1025
+ default_enabled: true,
1026
+ });
1027
+ }
1028
+ else {
1029
+ // Plain-English rule — parse it
1030
+ const parsed = parseNaturalLanguageRule(rule.description, i);
1031
+ if (parsed) {
1032
+ customGuards.push({
1033
+ id: parsed.id,
1034
+ label: parsed.description,
1035
+ description: parsed.description,
1036
+ category: "world-file",
1037
+ enforcement: parsed.enforcement,
1038
+ immutable: false,
1039
+ intent_patterns: parsed.intent_patterns,
1040
+ default_enabled: true,
1041
+ });
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ // Build the response world definition for the UI
1047
+ const loadedWorld = {
1048
+ id: "custom-world",
1049
+ title: w.name || "Custom World",
1050
+ thesis: w.thesis || "User-defined world",
1051
+ stateVariables: w.state_variables || [],
1052
+ invariants: (w.invariants || []).map(inv => ({
1053
+ id: inv.id,
1054
+ description: inv.description,
1055
+ enforceable: inv.enforceable !== false,
1056
+ })),
1057
+ gates: (w.gates || []).map(g => ({
1058
+ id: g.id,
1059
+ label: g.label,
1060
+ condition: g.condition,
1061
+ severity: g.severity || "warning",
1062
+ })),
1063
+ rulesApplied: customGuards.length,
1064
+ };
1065
+ jsonResponse(res, 200, {
1066
+ status: "loaded",
1067
+ world: loadedWorld,
1068
+ rulesApplied: customGuards.length,
1069
+ message: `World "${loadedWorld.title}" loaded with ${loadedWorld.invariants.length} invariants, ${loadedWorld.gates.length} gates, and ${customGuards.length} rules.`,
1070
+ });
1071
+ }
1072
+ catch (err) {
1073
+ jsonResponse(res, 400, { error: "Invalid world file JSON" });
1074
+ }
1075
+ return;
1076
+ }
1077
+ // ── Export World File ──
1078
+ // Exports the current world configuration (base world + custom rules + overrides) as a world file
1079
+ if (req.url === "/api/export-world" && req.method === "GET") {
1080
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
1081
+ const worldId = currentSession.world || "trading";
1082
+ let baseWorld;
1083
+ try {
1084
+ baseWorld = resolveWorld(worldId);
1085
+ }
1086
+ catch {
1087
+ baseWorld = null;
1088
+ }
1089
+ const exportedWorld = {
1090
+ name: baseWorld?.title || worldId,
1091
+ thesis: baseWorld?.world?.thesis || "Exported world",
1092
+ state_variables: baseWorld?.world?.state_variables || [],
1093
+ invariants: baseWorld?.world?.invariants || [],
1094
+ gates: baseWorld?.world?.gates || [],
1095
+ rules: customGuards.map(g => ({
1096
+ id: g.id,
1097
+ description: g.description,
1098
+ enforcement: g.enforcement,
1099
+ intent_patterns: g.intent_patterns,
1100
+ })),
1101
+ };
1102
+ res.writeHead(200, {
1103
+ "Content-Type": "application/json",
1104
+ "Content-Disposition": `attachment; filename="${worldId}-world.json"`,
1105
+ "Access-Control-Allow-Origin": "*",
1106
+ });
1107
+ res.end(JSON.stringify({ world: exportedWorld }, null, 2));
1108
+ return;
1109
+ }
1110
+ // ── Session Reporting Endpoints ──
1111
+ // Connect the serve runtime to the enforce reporting pipeline.
1112
+ // Users can request reports, stats, and recommendations from live governance data.
1113
+ // GET /api/session — current session stats (lightweight)
1114
+ if (req.url === "/api/session" && req.method === "GET") {
1115
+ const evals = currentSession.evaluations;
1116
+ const blocked = evals.filter(e => e.decision === "BLOCK").length;
1117
+ const modified = evals.filter(e => e.decision === "MODIFY").length;
1118
+ const allowed = evals.filter(e => e.decision === "ALLOW").length;
1119
+ const uniqueActors = [...new Set(evals.map(e => e.actor))];
1120
+ const uniqueActions = [...new Set(evals.map(e => e.action))];
1121
+ const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
1122
+ jsonResponse(res, 200, {
1123
+ sessionId: currentSession.id,
1124
+ startedAt: currentSession.startedAt,
1125
+ world: currentSession.world,
1126
+ guardCount: currentSession.guardCount,
1127
+ evaluations: {
1128
+ total: evals.length,
1129
+ blocked,
1130
+ modified,
1131
+ allowed,
1132
+ },
1133
+ agents: uniqueActors,
1134
+ actionTypes: uniqueActions,
1135
+ triggeredRules,
1136
+ historyCount: sessionHistory.length,
1137
+ });
1138
+ return;
1139
+ }
1140
+ // GET /api/session/report — full enforcement report (text)
1141
+ if (req.url === "/api/session/report" && req.method === "GET") {
1142
+ const report = synthesizeSessionReport();
1143
+ // Format as human-readable text using the same style as enforce CLI
1144
+ const lines = [];
1145
+ lines.push("");
1146
+ lines.push(" LIVE GOVERNANCE — ENFORCEMENT REPORT");
1147
+ lines.push(" " + "=".repeat(70));
1148
+ lines.push(` Session: ${report.sessionId}`);
1149
+ lines.push(` Scenario: ${report.scenario}`);
1150
+ lines.push(` Sessions: ${report.runs.length}`);
1151
+ lines.push(` Generated: ${report.generatedAt}`);
1152
+ lines.push("");
1153
+ if (report.runs.length > 0) {
1154
+ lines.push(" RUN HISTORY");
1155
+ lines.push(" " + "-".repeat(70));
1156
+ lines.push(` ${"#".padEnd(4)} ${"World".padEnd(25)} ${"Rules".padEnd(8)} ${"Evals".padEnd(8)} ${"Blocked".padEnd(10)} ${"Effectiveness"}`);
1157
+ lines.push(" " + "-".repeat(70));
1158
+ for (const run of report.runs) {
1159
+ const name = run.worldName.length > 23 ? run.worldName.slice(0, 22) + "…" : run.worldName;
1160
+ 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)}%`);
1161
+ }
1162
+ lines.push("");
1163
+ lines.push(" DIVERGENCE ANALYSIS");
1164
+ lines.push(" " + "-".repeat(70));
1165
+ lines.push(` Stability trend: ${report.divergence.stabilityTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
1166
+ lines.push(` Collapse trend: ${report.divergence.collapseTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
1167
+ lines.push(` Effectiveness trend: ${report.divergence.effectivenessTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
1168
+ lines.push(` Total divergence: ${report.divergence.totalDivergence}`);
1169
+ lines.push("");
1170
+ lines.push(` ${report.divergence.narrative}`);
1171
+ // Blocked action breakdown
1172
+ const allEvals = [
1173
+ ...sessionHistory.flatMap(s => s.evaluations),
1174
+ ...currentSession.evaluations,
1175
+ ];
1176
+ const blockedEvals = allEvals.filter(e => e.decision === "BLOCK");
1177
+ if (blockedEvals.length > 0) {
1178
+ lines.push("");
1179
+ lines.push(" BLOCKED ACTIONS");
1180
+ lines.push(" " + "-".repeat(70));
1181
+ // Group by rule
1182
+ const byRule = {};
1183
+ for (const e of blockedEvals) {
1184
+ const key = e.ruleId ?? "unknown";
1185
+ if (!byRule[key])
1186
+ byRule[key] = { count: 0, actors: new Set(), actions: new Set() };
1187
+ byRule[key].count++;
1188
+ byRule[key].actors.add(e.actor);
1189
+ byRule[key].actions.add(e.action);
1190
+ }
1191
+ const sorted = Object.entries(byRule).sort((a, b) => b[1].count - a[1].count);
1192
+ for (const [ruleId, data] of sorted) {
1193
+ lines.push(` ${ruleId}: ${data.count} blocks across ${data.actors.size} agent(s)`);
1194
+ lines.push(` Actions: ${[...data.actions].join(", ")}`);
1195
+ }
1196
+ }
1197
+ // Recommendations (deterministic, no LLM needed)
1198
+ lines.push("");
1199
+ lines.push(" RECOMMENDATIONS");
1200
+ lines.push(" " + "-".repeat(70));
1201
+ const best = report.runs[report.divergence.bestIteration - 1];
1202
+ if (best) {
1203
+ const totalEvals = best.governanceStats.totalEvaluations;
1204
+ const blockRate = totalEvals > 0 ? best.governanceStats.verdicts.block / totalEvals : 0;
1205
+ if (blockRate > 0.5) {
1206
+ lines.push(" HIGH BLOCK RATE — Rules may be too aggressive.");
1207
+ lines.push(" Try relaxing constraints or adding MODIFY actions instead of hard blocks.");
1208
+ lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
1209
+ }
1210
+ else if (blockRate < 0.05 && totalEvals > 20) {
1211
+ lines.push(" LOW BLOCK RATE — Rules may be too permissive.");
1212
+ lines.push(" Try adding content-specific guards or lowering thresholds.");
1213
+ lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
1214
+ }
1215
+ else if (blockRate > 0) {
1216
+ lines.push(` Governance is active: ${(blockRate * 100).toFixed(0)}% of actions blocked.`);
1217
+ lines.push(" To iterate: apply different rules via POST /api/apply-rules, then POST /api/session/reset.");
1218
+ lines.push(" This creates a new session for side-by-side comparison.");
1219
+ }
1220
+ if (report.runs.length === 1) {
1221
+ lines.push("");
1222
+ lines.push(" EXPERIMENT: Apply different rules and reset to compare sessions.");
1223
+ lines.push(" This will show divergence: how different governance changes outcomes.");
1224
+ }
1225
+ }
1226
+ lines.push("");
1227
+ lines.push(" RECOMMENDATION");
1228
+ lines.push(" " + "-".repeat(70));
1229
+ lines.push(` ${report.recommendation}`);
1230
+ }
1231
+ lines.push("");
1232
+ lines.push(" " + "=".repeat(70));
1233
+ lines.push(" NeuroVerse Policy Enforcement System — Live Governance");
1234
+ lines.push(" Design rules. Run reality. See what changes.");
1235
+ lines.push(" " + "=".repeat(70));
1236
+ lines.push("");
1237
+ res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
1238
+ res.end(lines.join("\n"));
1239
+ return;
1240
+ }
1241
+ // GET /api/session/report.json — full enforcement report (JSON)
1242
+ if (req.url === "/api/session/report.json" && req.method === "GET") {
1243
+ const report = synthesizeSessionReport();
1244
+ jsonResponse(res, 200, report);
1245
+ return;
1246
+ }
1247
+ // GET /api/session/evaluations — raw evaluation log
1248
+ if (req.url === "/api/session/evaluations" && req.method === "GET") {
1249
+ jsonResponse(res, 200, {
1250
+ sessionId: currentSession.id,
1251
+ count: currentSession.evaluations.length,
1252
+ evaluations: currentSession.evaluations.slice(-200), // last 200
1253
+ });
1254
+ return;
1255
+ }
1256
+ // POST /api/session/reset — snapshot current session and start fresh
1257
+ // This is how users create multi-run comparisons:
1258
+ // 1. Apply rules A → run simulation → POST /api/session/reset
1259
+ // 2. Apply rules B → run simulation → GET /api/session/report
1260
+ // Now the report shows divergence between rules A and rules B.
1261
+ if (req.url === "/api/session/reset" && req.method === "POST") {
1262
+ if (currentSession.evaluations.length > 0) {
1263
+ sessionHistory.push({ ...currentSession, evaluations: [...currentSession.evaluations] });
1264
+ }
1265
+ const body = await readBody(req).catch(() => "{}");
1266
+ const opts = JSON.parse(body || "{}");
1267
+ currentSession = {
1268
+ id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
1269
+ startedAt: new Date().toISOString(),
1270
+ world: opts.world ?? currentSession.world,
1271
+ guardCount: customGuards.length,
1272
+ evaluations: [],
1273
+ };
1274
+ // Reset bridge metrics counters
1275
+ bridgeEvalCount = 0;
1276
+ bridgeBlockCount = 0;
1277
+ bridgeModifyCount = 0;
1278
+ jsonResponse(res, 200, {
1279
+ status: "reset",
1280
+ newSessionId: currentSession.id,
1281
+ previousSessions: sessionHistory.length,
1282
+ message: `Session reset. ${sessionHistory.length} session(s) in history. Apply new rules and evaluate to compare.`,
1283
+ });
1284
+ return;
1285
+ }
1286
+ // POST /api/session/save — save session as experiment with lineage
1287
+ if (req.url === "/api/session/save" && req.method === "POST") {
1288
+ const report = synthesizeSessionReport();
1289
+ if (report.runs.length === 0) {
1290
+ jsonResponse(res, 400, { error: "No evaluations to save. Send actions to /api/evaluate first." });
1291
+ return;
1292
+ }
1293
+ const experiment = {
1294
+ id: `exp-live-${Date.now().toString(36)}`,
1295
+ savedAt: new Date().toISOString(),
1296
+ scenario: report.scenario,
1297
+ source: "live-governance",
1298
+ sessions: report.runs.length,
1299
+ totalEvaluations: report.runs.reduce((sum, r) => sum + r.governanceStats.totalEvaluations, 0),
1300
+ metrics: {
1301
+ stability: report.runs[report.divergence.bestIteration - 1]?.metrics.stabilityScore ?? 0,
1302
+ effectiveness: report.runs[report.divergence.bestIteration - 1]?.comparison.governanceEffectiveness ?? 0,
1303
+ collapseProbability: report.runs[report.divergence.bestIteration - 1]?.metrics.collapseProbability ?? 0,
1304
+ },
1305
+ divergence: report.divergence,
1306
+ recommendation: report.recommendation,
1307
+ report,
1308
+ };
1309
+ // Save to experiments/ directory
1310
+ try {
1311
+ if (!fs.existsSync("experiments"))
1312
+ fs.mkdirSync("experiments", { recursive: true });
1313
+ const filePath = path.join("experiments", `${experiment.id}.json`);
1314
+ fs.writeFileSync(filePath, JSON.stringify(experiment, null, 2));
1315
+ jsonResponse(res, 200, {
1316
+ status: "saved",
1317
+ experimentId: experiment.id,
1318
+ filePath,
1319
+ metrics: experiment.metrics,
1320
+ message: `Saved to ${filePath}. View with: npx nv-sim enforce --load ${filePath}`,
1321
+ });
1322
+ }
1323
+ catch (err) {
1324
+ jsonResponse(res, 500, { error: "Failed to save experiment", detail: String(err) });
1325
+ }
1326
+ return;
1327
+ }
1328
+ // List available live adapters
1329
+ if (req.url === "/api/adapters" && req.method === "GET") {
1330
+ const adapters = Object.values(liveAdapter_1.ADAPTER_REGISTRY).map(a => ({
1331
+ id: a.id,
1332
+ label: a.label,
1333
+ description: a.description,
1334
+ }));
1335
+ jsonResponse(res, 200, { adapters });
1336
+ return;
1337
+ }
1338
+ // Run simulation via live adapter (external process)
1339
+ if (req.url === "/api/run-live" && req.method === "POST") {
1340
+ if (isRunning) {
1341
+ jsonResponse(res, 409, { error: "Simulation already running" });
1342
+ return;
1343
+ }
1344
+ try {
1345
+ const body = await readBody(req);
1346
+ const payload = JSON.parse(body);
1347
+ const adapter = (0, liveAdapter_1.createAdapter)(payload.adapterId, payload.options);
1348
+ if (!adapter) {
1349
+ jsonResponse(res, 400, { error: `Unknown adapter: ${payload.adapterId}` });
1350
+ return;
1351
+ }
1352
+ isRunning = true;
1353
+ jsonResponse(res, 200, { status: "started", adapter: payload.adapterId });
1354
+ // Resolve world for governance evaluation
1355
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
1356
+ const resolved = resolveWorld(payload.worldId ?? "trading");
1357
+ const world = { ...resolved.world };
1358
+ if (payload.stateOverrides) {
1359
+ const updatedVars = world.state_variables.map(sv => {
1360
+ if (payload.stateOverrides && sv.id in payload.stateOverrides) {
1361
+ return { ...sv, default_value: payload.stateOverrides[sv.id] };
1362
+ }
1363
+ return sv;
1364
+ });
1365
+ world.state_variables = updatedVars;
1366
+ }
1367
+ const request = {
1368
+ scenario: resolved.scenario,
1369
+ stakeholders: resolved.stakeholders,
1370
+ assumptions: resolved.assumptions,
1371
+ constraints: resolved.constraints,
1372
+ depth: resolved.depth,
1373
+ swarm: resolved.swarm,
1374
+ };
1375
+ const nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, world);
1376
+ // Send meta event with adapter source
1377
+ broadcast({
1378
+ type: "meta",
1379
+ source: payload.adapterId,
1380
+ scenario: resolved.scenario,
1381
+ worldThesis: world.thesis,
1382
+ agents: [], // will be populated dynamically from adapter
1383
+ invariants: world.invariants.map(inv => ({ id: inv.id, description: inv.description })),
1384
+ gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
1385
+ narrativeEvents: [],
1386
+ totalRounds: 0,
1387
+ });
1388
+ // Listen for rounds from the adapter
1389
+ adapter.on("round", (liveRound) => {
1390
+ // Build lookup of original verdicts from adapter's adaptation data.
1391
+ // When an external bridge (e.g. MiroFish) already applied governance,
1392
+ // the verdict lives in adaptation.deltas — use it instead of re-evaluating
1393
+ // (re-evaluating the MODIFIED action would return ALLOW, hiding the BLOCK).
1394
+ const adaptationByAgent = new Map();
1395
+ if (liveRound.adaptation?.deltas) {
1396
+ for (const delta of liveRound.adaptation.deltas) {
1397
+ adaptationByAgent.set(delta.agent, {
1398
+ status: delta.decision,
1399
+ reason: delta.reason,
1400
+ ruleId: delta.rule,
1401
+ });
1402
+ }
1403
+ }
1404
+ const reactions = liveRound.agentActions.map(a => {
1405
+ // Priority: per-action verdict > adaptation delta > local re-evaluation
1406
+ const actionVerdict = a.verdict ? { status: a.verdict.status, reason: a.verdict.reason, ruleId: a.verdict.rule } : null;
1407
+ const bridgeVerdict = actionVerdict ?? adaptationByAgent.get(a.agent);
1408
+ let verdict;
1409
+ if (bridgeVerdict) {
1410
+ verdict = bridgeVerdict;
1411
+ }
1412
+ else {
1413
+ const guard = (0, worldBridge_1.evaluateScenarioGuard)({ ...request, scenario: `[R${liveRound.round}] ${a.agent}: ${a.action}` }, nvWorld, { trace: true, level: "standard" });
1414
+ verdict = { status: guard.status, reason: guard.reason, ruleId: guard.ruleId };
1415
+ }
1416
+ return {
1417
+ stakeholder_id: a.agent,
1418
+ reaction: a.action,
1419
+ impact: a.impact,
1420
+ confidence: a.confidence ?? 0.5,
1421
+ trigger: liveRound.source,
1422
+ verdict,
1423
+ };
1424
+ });
1425
+ const interventionCount = reactions.filter(r => r.verdict.status !== "ALLOW").length;
1426
+ broadcast({
1427
+ type: "round",
1428
+ round: liveRound.round,
1429
+ totalRounds: 0, // unknown for live streams
1430
+ phase: "governed",
1431
+ reactions,
1432
+ avgImpact: reactions.reduce((s, r) => s + r.impact, 0) / (reactions.length || 1),
1433
+ maxVolatility: Math.max(0, ...reactions.map(r => Math.abs(r.impact))),
1434
+ dynamics: liveRound.emergentDynamics ?? [],
1435
+ interventionCount,
1436
+ });
1437
+ });
1438
+ adapter.on("complete", () => {
1439
+ isRunning = false;
1440
+ });
1441
+ adapter.on("error", (err) => {
1442
+ broadcast({ type: "complete", result: { error: String(err) } });
1443
+ isRunning = false;
1444
+ });
1445
+ adapter.start().catch(err => {
1446
+ broadcast({ type: "complete", result: { error: String(err) } });
1447
+ isRunning = false;
1448
+ });
1449
+ }
1450
+ catch (err) {
1451
+ jsonResponse(res, 400, { error: "Invalid request body" });
1452
+ }
1453
+ return;
1454
+ }
1455
+ // ── AUDIT TRAIL ENDPOINTS ──
1456
+ // GET /api/audit — summary of current session's audit trail
1457
+ if (req.url === "/api/audit" && req.method === "GET") {
1458
+ jsonResponse(res, 200, auditTrail.summary());
1459
+ return;
1460
+ }
1461
+ // GET /api/audit/entries — all entries from current session (with optional filters)
1462
+ if (req.url?.startsWith("/api/audit/entries") && req.method === "GET") {
1463
+ const url = new URL(req.url, `http://localhost:${port}`);
1464
+ const entries = auditTrail.query({
1465
+ type: url.searchParams.get("type") ?? undefined,
1466
+ agent: url.searchParams.get("agent") ?? undefined,
1467
+ verdict: url.searchParams.get("verdict") ?? undefined,
1468
+ runId: url.searchParams.get("runId") ?? undefined,
1469
+ after: url.searchParams.get("after") ?? undefined,
1470
+ before: url.searchParams.get("before") ?? undefined,
1471
+ });
1472
+ jsonResponse(res, 200, {
1473
+ sessionId: auditTrail.getSessionId(),
1474
+ count: entries.length,
1475
+ entries,
1476
+ });
1477
+ return;
1478
+ }
1479
+ // GET /api/audit/entries/text — human-readable audit trail
1480
+ if (req.url?.startsWith("/api/audit/entries/text") && req.method === "GET") {
1481
+ const entries = auditTrail.readAll();
1482
+ res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
1483
+ res.end((0, auditTrace_1.formatAuditTrail)(entries, { verbose: true }));
1484
+ return;
1485
+ }
1486
+ // GET /api/audit/sessions — list all audit sessions on disk
1487
+ if (req.url === "/api/audit/sessions" && req.method === "GET") {
1488
+ const sessions = (0, auditTrace_1.listAuditSessions)();
1489
+ jsonResponse(res, 200, {
1490
+ sessions: sessions.map((id) => {
1491
+ const trail = (0, auditTrace_1.loadAuditSession)(id);
1492
+ return trail.summary();
1493
+ }),
1494
+ });
1495
+ return;
1496
+ }
1497
+ // GET /api/audit/sessions/:id — load a specific session's audit trail
1498
+ const auditSessionMatch = req.url?.match(/^\/api\/audit\/sessions\/([^/]+)$/);
1499
+ if (auditSessionMatch && req.method === "GET") {
1500
+ const sessionId = decodeURIComponent(auditSessionMatch[1]);
1501
+ const trail = (0, auditTrace_1.loadAuditSession)(sessionId);
1502
+ const entries = trail.readAll();
1503
+ if (entries.length === 0) {
1504
+ jsonResponse(res, 404, { error: `Audit session "${sessionId}" not found or empty` });
1505
+ }
1506
+ else {
1507
+ jsonResponse(res, 200, {
1508
+ ...trail.summary(),
1509
+ entries,
1510
+ });
1511
+ }
1512
+ return;
1513
+ }
1514
+ // GET /api/audit/search — search across ALL sessions
1515
+ if (req.url?.startsWith("/api/audit/search") && req.method === "GET") {
1516
+ const url = new URL(req.url, `http://localhost:${port}`);
1517
+ const results = (0, auditTrace_1.searchAuditTrails)({
1518
+ type: url.searchParams.get("type") ?? undefined,
1519
+ agent: url.searchParams.get("agent") ?? undefined,
1520
+ verdict: url.searchParams.get("verdict") ?? undefined,
1521
+ after: url.searchParams.get("after") ?? undefined,
1522
+ before: url.searchParams.get("before") ?? undefined,
1523
+ });
1524
+ jsonResponse(res, 200, {
1525
+ count: results.length,
1526
+ entries: results,
1527
+ });
1528
+ return;
1529
+ }
1530
+ // Serve the interactive dashboard
1531
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1532
+ res.end(INTERACTIVE_DASHBOARD_HTML);
1533
+ });
1534
+ server.listen(port, () => {
1535
+ onReady(`http://localhost:${port}`);
1536
+ });
1537
+ return { server };
1538
+ }
1539
+ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1540
+ <html lang="en">
1541
+ <head>
1542
+ <meta charset="UTF-8">
1543
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1544
+ <title>NV-SIM — Scenario Control Platform</title>
1545
+ <style>
1546
+ /* ── Theme Variables ── */
1547
+ :root {
1548
+ --bg-primary: #0f0f0f;
1549
+ --bg-secondary: #141414;
1550
+ --bg-surface: #1a1a1a;
1551
+ --bg-elevated: #222;
1552
+ --border: #2a2a2a;
1553
+ --border-subtle: #333;
1554
+ --text-primary: #f0f0f0;
1555
+ --text-secondary: #b0b0b0;
1556
+ --text-muted: #888;
1557
+ --text-faint: #666;
1558
+ --accent: #818cf8;
1559
+ --accent-bg: #1e1e3a;
1560
+ --green: #4ade80;
1561
+ --green-bg: #0a2a14;
1562
+ --red: #f87171;
1563
+ --red-bg: #2d0a0a;
1564
+ --yellow: #fbbf24;
1565
+ --yellow-bg: #2d2006;
1566
+ --blue: #60a5fa;
1567
+ --blue-bg: #1e293b;
1568
+ --purple: #a78bfa;
1569
+ }
1570
+ body.light {
1571
+ --bg-primary: #f5f5f5;
1572
+ --bg-secondary: #eaeaea;
1573
+ --bg-surface: #e0e0e0;
1574
+ --bg-elevated: #d4d4d4;
1575
+ --border: #c0c0c0;
1576
+ --border-subtle: #b0b0b0;
1577
+ --text-primary: #1a1a1a;
1578
+ --text-secondary: #444;
1579
+ --text-muted: #666;
1580
+ --text-faint: #888;
1581
+ --accent: #6366f1;
1582
+ --accent-bg: #e8e8ff;
1583
+ --green: #16a34a;
1584
+ --green-bg: #dcfce7;
1585
+ --red: #dc2626;
1586
+ --red-bg: #fee2e2;
1587
+ --yellow: #ca8a04;
1588
+ --yellow-bg: #fef9c3;
1589
+ --blue: #2563eb;
1590
+ --blue-bg: #dbeafe;
1591
+ --purple: #7c3aed;
1592
+ }
1593
+
1594
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1595
+ 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; }
1596
+ .header { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
1597
+ .header h1 { font-size: 15px; color: var(--text-primary); }
1598
+ .header .sub { font-size: 11px; color: var(--text-muted); margin-left: 12px; }
1599
+ .header-right { display: flex; align-items: center; gap: 12px; }
1600
+ .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; }
1601
+ .theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
1602
+ .status { font-size: 11px; padding: 3px 10px; border-radius: 10px; }
1603
+ .status.idle { background: var(--accent-bg); color: var(--accent); }
1604
+ .status.live { background: var(--green-bg); color: var(--green); animation: pulse 2s infinite; }
1605
+ .status.complete { background: var(--accent-bg); color: var(--accent); }
1606
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
1607
+
1608
+ .layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }
1609
+
1610
+ /* LEFT PANEL — Controls */
1611
+ .controls { background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px; }
1612
+ .ctrl-section { margin-bottom: 20px; }
1613
+ .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); }
1614
+
1615
+ .ctrl-row { margin-bottom: 12px; }
1616
+ .ctrl-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; display: flex; justify-content: space-between; }
1617
+ .ctrl-label .val { color: var(--text-primary); font-weight: 600; }
1618
+
1619
+ input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: var(--bg-elevated); border-radius: 2px; outline: none; }
1620
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; }
1621
+ input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: none; }
1622
+
1623
+ 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; }
1624
+
1625
+ .toggle-row { display: flex; align-items: center; gap: 8px; }
1626
+ .toggle { position: relative; width: 36px; height: 20px; background: var(--border-subtle); border-radius: 10px; cursor: pointer; transition: background 0.2s; }
1627
+ .toggle.on { background: var(--green); }
1628
+ .toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
1629
+ .toggle.on::after { transform: translateX(16px); }
1630
+ .toggle-label { font-size: 12px; color: var(--text-secondary); }
1631
+
1632
+ .inject-row { display: flex; gap: 6px; margin-bottom: 6px; }
1633
+ .inject-row select { flex: 1; }
1634
+ .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; }
1635
+ .inject-list { margin-bottom: 8px; }
1636
+ .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; }
1637
+ .inject-item .remove { color: var(--red); cursor: pointer; }
1638
+
1639
+ .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; }
1640
+ .btn-run { background: var(--green); color: #0a0a0a; }
1641
+ .btn-run:hover { background: #22c55e; }
1642
+ .btn-run:disabled { background: var(--bg-elevated); color: var(--text-faint); cursor: not-allowed; }
1643
+ .btn-add { background: var(--bg-elevated); color: var(--accent); padding: 6px; font-size: 11px; }
1644
+ .btn-add:hover { background: var(--accent-bg); }
1645
+
1646
+ .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; }
1647
+ .scenario-btn:hover { border-color: var(--accent); background: var(--accent-bg); }
1648
+ .scenario-btn .stitle { font-weight: 600; color: var(--text-primary); }
1649
+ .scenario-btn .sdesc { color: var(--text-muted); font-size: 10px; margin-top: 2px; }
1650
+
1651
+ /* Save variant */
1652
+ .btn-save { background: var(--accent-bg); color: var(--accent); margin-top: 0; }
1653
+ .btn-save:hover { background: var(--accent-bg); filter: brightness(1.2); }
1654
+ .btn-confirm { flex: 1; background: var(--green); color: #0a0a0a; padding: 7px; font-size: 11px; }
1655
+ .btn-cancel { flex: 1; background: var(--bg-elevated); color: var(--text-muted); padding: 7px; font-size: 11px; }
1656
+ .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; }
1657
+ .save-input:focus { border-color: var(--accent); outline: none; }
1658
+
1659
+ /* Variant cards */
1660
+ .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; }
1661
+ .variant-card:hover { border-color: var(--green); }
1662
+ .variant-card .vname { font-size: 12px; font-weight: 600; color: var(--text-primary); }
1663
+ .variant-card .vdesc { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
1664
+ .variant-card .vmeta { font-size: 10px; color: var(--text-faint); margin-top: 4px; }
1665
+ .variant-card .vmeta .vresult { color: var(--green); }
1666
+ .variant-card .vdelete { position: absolute; top: 6px; right: 8px; color: var(--red); font-size: 10px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
1667
+ .variant-card:hover .vdelete { opacity: 1; }
1668
+ .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; }
1669
+
1670
+ /* RIGHT PANEL — Simulation viewer */
1671
+ .viewer { display: grid; grid-template-rows: auto auto auto auto auto; overflow-y: auto; height: 100%; gap: 0; }
1672
+
1673
+ /* Outcome panel */
1674
+ .outcome-panel { padding: 20px 24px 16px; border-bottom: 1px solid var(--border); }
1675
+ .outcome-statement { font-size: 16px; font-weight: 600; color: var(--text-primary); line-height: 1.4; margin-bottom: 12px; }
1676
+ .outcome-empty { font-size: 13px; color: var(--text-faint); font-style: italic; }
1677
+ .confidence-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 8px; }
1678
+ .confidence-card { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 8px 10px; }
1679
+ .confidence-card .cc-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
1680
+ .confidence-card .cc-value { font-size: 13px; font-weight: 600; }
1681
+ .confidence-card .cc-value.good { color: var(--green); }
1682
+ .confidence-card .cc-value.warn { color: var(--yellow); }
1683
+ .confidence-card .cc-value.bad { color: var(--red); }
1684
+ .outcome-context { font-size: 10px; color: var(--text-faint); margin-top: 6px; }
1685
+
1686
+ /* Behavior panel */
1687
+ .behavior-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
1688
+ .behavior-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
1689
+ .behavior-shifts { margin-bottom: 10px; }
1690
+ .behavior-shift-item { font-size: 12px; color: var(--text-secondary); padding: 4px 0 4px 12px; border-left: 2px solid var(--accent); line-height: 1.5; margin-bottom: 4px; }
1691
+ .behavior-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
1692
+
1693
+ .activity-toggle { font-size: 10px; color: var(--text-muted); cursor: pointer; padding: 6px 0; margin-top: 4px; }
1694
+ .activity-toggle:hover { color: var(--text-secondary); }
1695
+ .activity-timeline { display: none; max-height: 180px; overflow-y: auto; margin-top: 6px; }
1696
+ .activity-timeline.open { display: block; }
1697
+ .activity-item { font-size: 11px; color: var(--text-secondary); padding: 3px 0; border-bottom: 1px solid var(--border); }
1698
+ .activity-item:last-child { border-bottom: none; }
1699
+ .activity-agent { color: var(--blue); font-weight: 500; }
1700
+
1701
+ /* Why panel */
1702
+ .why-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
1703
+ .why-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
1704
+ .why-item { font-size: 12px; color: var(--text-secondary); padding: 4px 0 4px 12px; border-left: 2px solid var(--green); line-height: 1.5; margin-bottom: 4px; }
1705
+ .why-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
1706
+
1707
+ /* Export bar */
1708
+ .export-bar { display: flex; gap: 8px; padding: 12px 24px; border-bottom: 1px solid var(--border); background: var(--bg-secondary); }
1709
+ .export-btn { padding: 6px 14px; font-size: 11px; font-weight: 600; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-surface); color: var(--text-secondary); cursor: pointer; font-family: inherit; transition: all 0.2s; }
1710
+ .export-btn:hover { background: var(--bg-elevated); color: var(--text-primary); }
1711
+ .audit-btn { margin-left: auto; color: var(--text-faint); }
1712
+
1713
+ /* Audit overlay */
1714
+ .audit-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; background: rgba(0,0,0,0.7); }
1715
+ .audit-overlay.open { display: flex; align-items: center; justify-content: center; }
1716
+ .audit-modal { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 8px; width: 90%; max-width: 900px; max-height: 85vh; overflow-y: auto; padding: 0; }
1717
+ .audit-header { display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg-primary); z-index: 1; }
1718
+ .audit-header h2 { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; }
1719
+ .audit-close { margin-left: auto; padding: 4px 12px; font-size: 12px; background: none; border: 1px solid var(--border); border-radius: 4px; color: var(--text-muted); cursor: pointer; font-family: inherit; }
1720
+ .audit-close:hover { color: var(--text-primary); }
1721
+ .audit-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
1722
+ .audit-section:last-child { border-bottom: none; }
1723
+ .audit-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
1724
+ .audit-section .rule-card { margin-bottom: 6px; }
1725
+
1726
+ /* Print styles for PDF export */
1727
+ @media print {
1728
+ body { background: #fff !important; color: #111 !important; }
1729
+ .controls, .export-bar, .audit-overlay, .activity-toggle { display: none !important; }
1730
+ .viewer { overflow: visible !important; height: auto !important; }
1731
+ .outcome-panel, .behavior-panel, .why-panel { border: 1px solid #ddd !important; margin-bottom: 8px; border-radius: 6px; }
1732
+ .confidence-card { border: 1px solid #ddd !important; }
1733
+ * { color: #111 !important; background: #fff !important; border-color: #ddd !important; }
1734
+ .cc-value.good { color: #16a34a !important; }
1735
+ .cc-value.warn { color: #ca8a04 !important; }
1736
+ .cc-value.bad { color: #dc2626 !important; }
1737
+ }
1738
+ .center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
1739
+ .verdict { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-left: 4px; }
1740
+ .verdict.ALLOW { background: var(--green-bg); color: var(--green); }
1741
+ .verdict.BLOCK { background: var(--red-bg); color: var(--red); }
1742
+ .verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
1743
+
1744
+ /* Simulation Trace (audit only) */
1745
+ .trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
1746
+ .trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
1747
+ .trace-round-header:hover { background: var(--bg-elevated); }
1748
+ .trace-phase { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
1749
+ .trace-phase.baseline { background: var(--blue-bg); color: var(--blue); }
1750
+ .trace-phase.governed { background: var(--green-bg); color: var(--green); }
1751
+ .trace-round-label { font-size: 11px; color: var(--text-primary); font-weight: 600; }
1752
+ .trace-round-metrics { margin-left: auto; font-size: 10px; color: var(--text-muted); display: flex; gap: 10px; }
1753
+ .trace-body { padding: 0 10px 8px; }
1754
+ .trace-body[data-collapsed="true"] { display: none; }
1755
+ .trace-section { margin-top: 6px; }
1756
+ .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; }
1757
+ .trace-section-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
1758
+ .trace-section-label.event::before { background: #f59e0b; }
1759
+ .trace-section-label.agents::before { background: #3b82f6; }
1760
+ .trace-section-label.governance::before { background: #10b981; }
1761
+ .trace-event-item { font-size: 10px; color: var(--text-primary); padding: 3px 0 3px 11px; border-left: 2px solid #f59e0b; margin-left: 2px; }
1762
+ .trace-event-severity { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; margin-left: 4px; }
1763
+ .trace-event-severity.major, .trace-event-severity.extreme { background: var(--red-bg); color: var(--red); }
1764
+ .trace-event-severity.moderate { background: var(--yellow-bg); color: var(--yellow); }
1765
+ .trace-event-severity.minor { background: var(--green-bg); color: var(--green); }
1766
+ .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; }
1767
+ .trace-agent-name { color: var(--blue); font-weight: 500; min-width: 80px; }
1768
+ .trace-agent-action { color: var(--text-secondary); flex: 1; }
1769
+ .trace-agent-impact { font-size: 9px; font-weight: 600; min-width: 36px; text-align: right; }
1770
+ .trace-agent-impact.positive { color: var(--green); }
1771
+ .trace-agent-impact.negative { color: var(--red); }
1772
+ .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; }
1773
+ .trace-gov-rule { font-size: 9px; color: var(--text-muted); font-family: monospace; }
1774
+ .trace-gov-reason { color: var(--text-secondary); flex: 1; }
1775
+ .trace-dynamics { font-size: 10px; color: var(--purple); padding: 2px 0 2px 11px; border-left: 2px solid #7c3aed; margin-left: 2px; font-style: italic; }
1776
+ .trace-arrow { color: var(--text-faint); font-size: 10px; text-align: center; padding: 2px 0; }
1777
+ .trace-empty { font-size: 10px; color: var(--text-faint); font-style: italic; padding: 4px 0; }
1778
+
1779
+ /* World info */
1780
+ .world-thesis { font-size: 11px; color: var(--text-secondary); font-style: italic; margin-bottom: 8px; }
1781
+ .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; }
1782
+ .rule-card:hover { border-color: var(--text-muted); }
1783
+ .rule-card.type-invariant { border-left: 4px solid var(--green); }
1784
+ .rule-card.type-gate { border-left: 4px solid var(--red); }
1785
+ .rule-card.type-warning { border-left: 4px solid var(--yellow); }
1786
+ .rule-card.type-modify { border-left: 4px solid #60a5fa; }
1787
+ .rule-card .rule-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
1788
+ .rule-card .rule-icon { font-size: 12px; flex-shrink: 0; }
1789
+ .rule-card .rule-title { font-size: 12px; font-weight: 600; color: var(--text-primary); }
1790
+ .rule-card .rule-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
1791
+ .rule-card .rule-meta { font-size: 10px; color: var(--text-muted); margin-top: 6px; opacity: 0.8; }
1792
+ .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); }
1793
+ .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)); }
1794
+ .rule-card .rule-impact.visible { display: block; }
1795
+ .rule-card .rule-impact .impact-stat { color: var(--text-primary); font-weight: 600; }
1796
+ .rule-card .rule-impact .impact-label { color: var(--text-muted); }
1797
+ .rule-card.user-rule { border-left-color: #818cf8; background: rgba(129, 140, 248, 0.05); }
1798
+ .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; }
1799
+
1800
+ /* Empty state */
1801
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-faint); }
1802
+ .empty-state .icon { font-size: 48px; margin-bottom: 12px; }
1803
+ .empty-state .msg { font-size: 13px; color: var(--text-muted); }
1804
+ .empty-state .hint { font-size: 11px; color: var(--text-faint); margin-top: 6px; }
1805
+
1806
+ /* System Shift Card */
1807
+ .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; }
1808
+ .system-shift.visible { display: block; }
1809
+ @keyframes fadeInUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
1810
+ .ss-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background: var(--green-bg); border-bottom: 1px solid var(--green-bg); }
1811
+ .ss-icon { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgba(74,222,128,0.4); }
1812
+ .ss-title { font-size: 11px; font-weight: 700; color: var(--green); text-transform: uppercase; letter-spacing: 1.5px; }
1813
+ .ss-rule { font-size: 13px; font-weight: 600; color: var(--text-primary); padding: 10px 14px 0; }
1814
+ .ss-body { padding: 10px 14px 14px; display: grid; gap: 8px; }
1815
+ .ss-section { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
1816
+ .ss-section-label { font-size: 9px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
1817
+ .ss-adapt-rate { font-size: 20px; font-weight: 700; color: var(--green); }
1818
+ .ss-adapt-desc { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
1819
+ .ss-shift-item { font-size: 11px; color: var(--text-secondary); padding: 2px 0; display: flex; align-items: center; gap: 6px; }
1820
+ .ss-shift-arrow { color: var(--green); font-weight: 600; }
1821
+ .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; }
1822
+ .ss-impact-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--text-secondary); padding: 2px 0; }
1823
+ .ss-impact-delta { color: var(--green); font-weight: 600; }
1824
+ .ss-impact-delta.negative { color: var(--red); }
1825
+ .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; }
1826
+ .ss-scale { font-size: 10px; color: var(--text-muted); padding: 0 14px 6px; }
1827
+ .ss-scale strong { color: var(--text-secondary); }
1828
+ .ss-flow { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 10px; color: var(--text-faint); }
1829
+ .ss-flow-arrow { color: var(--green); }
1830
+ .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; }
1831
+ .ss-raw-toggle:hover { color: var(--text-secondary); }
1832
+ .ss-raw-toggle .arrow { transition: transform 0.2s; }
1833
+ .ss-raw-toggle.open .arrow { transform: rotate(90deg); }
1834
+ .ss-raw-detail { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
1835
+ .ss-raw-detail.open { max-height: 200px; overflow-y: auto; }
1836
+ .ss-raw-list { padding: 6px 12px; }
1837
+ .ss-raw-item { font-size: 10px; color: var(--text-muted); padding: 2px 0; display: flex; gap: 6px; }
1838
+ .ss-raw-item .raw-agent { color: var(--text-secondary); min-width: 100px; }
1839
+ .ss-raw-item .raw-action { color: var(--text-secondary); flex: 1; }
1840
+ .ss-raw-item .raw-verdict { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; }
1841
+ .ss-raw-item .raw-verdict.BLOCK { background: var(--red-bg); color: var(--red); }
1842
+ .ss-raw-item .raw-verdict.MODIFY { background: var(--yellow-bg); color: var(--yellow); }
1843
+
1844
+ /* Integration Quick-Start (in controls panel) */
1845
+ .integrate-section { background: var(--accent-bg); border: 1px solid var(--accent-bg); border-radius: 6px; padding: 10px 12px; margin-top: 8px; }
1846
+ .integrate-section h4 { font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
1847
+ .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; }
1848
+ .integrate-code .kw { color: var(--accent); }
1849
+ .integrate-code .str { color: var(--green); }
1850
+ .integrate-code .comment { color: var(--text-muted); }
1851
+ .integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
1852
+ .integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
1853
+
1854
+ /* Language tabs for integration code */
1855
+ .lang-tabs { display: flex; gap: 2px; margin-bottom: 6px; }
1856
+ .lang-tab { padding: 4px 10px; font-size: 10px; font-weight: 600; color: var(--text-muted); background: transparent; border: 1px solid transparent; border-bottom: none; border-radius: 4px 4px 0 0; cursor: pointer; font-family: inherit; transition: all 0.2s; }
1857
+ .lang-tab:hover { color: var(--text-secondary); }
1858
+ .lang-tab.active { color: var(--accent); background: var(--bg-surface); border-color: var(--bg-elevated); }
1859
+ .lang-code-panel { display: none; }
1860
+ .lang-code-panel.active { display: block; }
1861
+ .integrate-hint { font-size: 10px; color: var(--text-muted); margin-top: 6px; line-height: 1.5; }
1862
+ .integrate-hint strong { color: var(--text-secondary); }
1863
+ .integrate-works { font-size: 10px; color: var(--text-faint); margin-top: 8px; line-height: 1.6; }
1864
+ .integrate-works strong { color: var(--text-muted); }
1865
+
1866
+ /* Framework selector tabs */
1867
+ .fw-tabs { display: flex; gap: 4px; margin-bottom: 10px; }
1868
+ .fw-tab { flex: 1; padding: 8px 6px; font-size: 10px; font-weight: 700; color: var(--text-muted); background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-family: inherit; transition: all 0.2s; text-align: center; line-height: 1.3; }
1869
+ .fw-tab:hover { border-color: var(--text-muted); color: var(--text-secondary); }
1870
+ .fw-tab.active { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
1871
+ .fw-tab .fw-sub { display: block; font-size: 8px; font-weight: 400; color: var(--text-faint); margin-top: 2px; }
1872
+ .fw-tab.active .fw-sub { color: var(--accent); opacity: 0.7; }
1873
+ .fw-panel { display: none; }
1874
+ .fw-panel.active { display: block; }
1875
+
1876
+ /* Architecture callout */
1877
+ .arch-callout { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; }
1878
+ .arch-callout .arch-title { font-size: 10px; font-weight: 700; color: var(--text-secondary); margin-bottom: 4px; }
1879
+ .arch-flow { font-size: 10px; color: var(--text-muted); font-family: monospace; line-height: 1.6; }
1880
+ .arch-flow .arch-yes { color: var(--green); }
1881
+ .arch-flow .arch-no { color: var(--red); }
1882
+ .arch-warn { font-size: 9px; color: var(--red); margin-top: 6px; padding: 4px 8px; background: #2d0606; border-radius: 4px; line-height: 1.5; }
1883
+
1884
+ /* Responsive / Adaptive Panels */
1885
+ @media (max-width: 1200px) {
1886
+ .layout { grid-template-columns: 300px 1fr; }
1887
+ .controls { padding: 12px; }
1888
+ }
1889
+ @media (max-width: 960px) {
1890
+ .layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; height: auto; min-height: 100vh; }
1891
+ .controls { border-right: none; border-bottom: 1px solid var(--border); max-height: 50vh; overflow-y: auto; }
1892
+ .viewer { min-height: 60vh; }
1893
+ .viewer-top { grid-template-columns: 1fr; }
1894
+ .viewer-mid { grid-template-columns: 1fr; }
1895
+ .metric-grid { grid-template-columns: repeat(2, 1fr); }
1896
+ }
1897
+ @media (max-width: 640px) {
1898
+ .header h1 { font-size: 14px; }
1899
+ .header .sub { font-size: 10px; }
1900
+ .controls { padding: 10px; }
1901
+ .ctrl-section h3 { font-size: 10px; }
1902
+ .confidence-grid { grid-template-columns: 1fr; }
1903
+ .agent-name { width: 80px; }
1904
+ .integrate-code { font-size: 9px; padding: 6px; }
1905
+ .scenario-btn { padding: 6px; }
1906
+ }
1907
+
1908
+ /* Rule editor */
1909
+ .rule-editor { margin-top: 8px; }
1910
+ .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; }
1911
+ .rule-input:focus { border-color: var(--accent); outline: none; }
1912
+ .rule-input::placeholder { color: var(--text-faint); }
1913
+ .btn-parse { background: var(--accent-bg); color: var(--accent); margin-top: 6px; padding: 8px; font-size: 11px; }
1914
+ .btn-parse:hover { filter: brightness(1.2); }
1915
+ .btn-parse:disabled { opacity: 0.5; cursor: not-allowed; }
1916
+ .parsed-rules { margin-top: 8px; }
1917
+ .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; }
1918
+ .parsed-rule:hover { border-color: var(--text-muted); }
1919
+ .parsed-rule.enforcement-block { border-left: 4px solid var(--red); }
1920
+ .parsed-rule.enforcement-allow { border-left: 4px solid var(--green); }
1921
+ .parsed-rule.enforcement-modify { border-left: 4px solid #60a5fa; }
1922
+ .parsed-rule.enforcement-warn { border-left: 4px solid var(--yellow); }
1923
+ .parsed-rule .pr-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
1924
+ .parsed-rule .pr-icon { font-size: 12px; }
1925
+ .parsed-rule .pr-action { font-weight: 700; font-size: 12px; color: var(--text-primary); text-transform: capitalize; }
1926
+ .parsed-rule .pr-desc { color: var(--text-secondary); font-size: 11px; line-height: 1.4; }
1927
+ .parsed-rule .pr-patterns { color: var(--text-muted); font-size: 10px; margin-top: 6px; opacity: 0.8; }
1928
+ .btn-apply-rules { background: var(--green); color: #0a0a0a; margin-top: 6px; padding: 8px; font-size: 11px; }
1929
+ .btn-apply-rules:hover { filter: brightness(0.9); }
1930
+ .rule-status { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
1931
+ .rule-status.success { color: var(--green); }
1932
+ .rule-status.error { color: var(--red); }
1933
+ .rule-examples { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.6; }
1934
+ .rule-examples code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
1935
+
1936
+ /* World Action Bar */
1937
+ .world-action-bar { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
1938
+ .btn-world-action { flex: 1; min-width: 0; padding: 6px 4px; font-size: 10px; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font-family: inherit; text-align: center; transition: all 0.2s; white-space: nowrap; }
1939
+ .btn-world-action:hover { border-color: var(--accent); color: var(--accent); }
1940
+ .btn-world-action.btn-export { color: var(--green); border-color: var(--green); opacity: 0.7; }
1941
+ .btn-world-action.btn-export:hover { opacity: 1; }
1942
+
1943
+ /* World Source Tabs */
1944
+ .world-source-tabs { display: flex; gap: 4px; }
1945
+ .ws-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 10px 6px; background: var(--bg-surface); border: 2px solid var(--border); border-radius: 6px; cursor: pointer; transition: all 0.2s; text-align: center; }
1946
+ .ws-tab:hover { border-color: var(--text-muted); }
1947
+ .ws-tab.active { border-color: var(--accent); background: var(--accent-bg); }
1948
+ .ws-tab input[type="radio"] { display: none; }
1949
+ .ws-label { font-size: 11px; font-weight: 700; color: var(--text-primary); }
1950
+ .ws-hint { font-size: 9px; color: var(--text-muted); margin-top: 2px; }
1951
+ .ws-tab.active .ws-label { color: var(--accent); }
1952
+
1953
+ /* World Source Panels */
1954
+ .world-source-panel { animation: fadeIn 0.2s ease; }
1955
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
1956
+
1957
+ /* Custom World Header */
1958
+ .custom-world-header { margin-bottom: 12px; }
1959
+ .world-name-input { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
1960
+ .world-name-input:focus { border-color: var(--accent); outline: none; }
1961
+ .world-thesis-input { width: 100%; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 11px; resize: none; height: 36px; }
1962
+ .world-thesis-input:focus { border-color: var(--accent); outline: none; }
1963
+
1964
+ /* Rule editor enhancements */
1965
+ .rule-editor-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600; }
1966
+ .rule-input-large { min-height: 120px; }
1967
+ .btn-generate-world { background: var(--accent); color: #fff; margin-top: 8px; padding: 10px; font-size: 12px; font-weight: 700; width: 100%; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
1968
+ .btn-generate-world:hover { filter: brightness(1.1); }
1969
+
1970
+ /* Upload Zone */
1971
+ .upload-zone { border: 2px dashed var(--border-subtle); border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.2s; margin-bottom: 12px; }
1972
+ .upload-zone:hover, .upload-zone.dragover { border-color: var(--accent); background: var(--accent-bg); }
1973
+ .upload-icon { font-size: 28px; margin-bottom: 8px; }
1974
+ .upload-label { font-size: 12px; color: var(--text-secondary); }
1975
+ .upload-or { font-size: 10px; color: var(--text-faint); margin: 8px 0; }
1976
+ .btn-upload-browse { background: var(--bg-elevated); color: var(--text-primary); border: 1px solid var(--border); padding: 6px 16px; border-radius: 4px; cursor: pointer; font-family: inherit; font-size: 11px; }
1977
+ .upload-paste-section { margin-bottom: 12px; }
1978
+ .btn-load-world { background: var(--green); color: #0a0a0a; width: 100%; padding: 10px; font-size: 12px; font-weight: 700; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
1979
+ .btn-load-world:hover { filter: brightness(0.9); }
1980
+
1981
+ /* Loaded World Card */
1982
+ .loaded-world-card { background: var(--green-bg); border: 1px solid var(--green); border-radius: 6px; padding: 12px; margin-top: 10px; }
1983
+ .lw-name { font-size: 13px; font-weight: 700; color: var(--green); }
1984
+ .lw-thesis { font-size: 11px; color: var(--text-secondary); margin-top: 4px; font-style: italic; }
1985
+ .lw-stats { font-size: 10px; color: var(--text-muted); margin-top: 6px; }
1986
+
1987
+ /* Schema Reference */
1988
+ .schema-ref { font-size: 10px; color: var(--text-muted); }
1989
+ .schema-item { padding: 3px 0; border-bottom: 1px solid var(--border); }
1990
+ .schema-item:last-child { border-bottom: none; }
1991
+ .schema-item code { color: var(--accent); background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; }
1992
+ </style>
1993
+ </head>
1994
+ <body>
1995
+ <div class="header">
1996
+ <div style="display:flex;align-items:center">
1997
+ <h1>NV-SIM</h1>
1998
+ <span class="sub">Governance Runtime</span>
1999
+ </div>
2000
+ <div class="header-right">
2001
+ <button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">Light Mode</button>
2002
+ <span id="status" class="status idle">Ready</span>
2003
+ </div>
2004
+ </div>
2005
+
2006
+ <div class="layout">
2007
+ <!-- LEFT: CONTROLS -->
2008
+ <div class="controls" id="controls-panel">
2009
+ <!-- World Action Bar -->
2010
+ <div class="world-action-bar">
2011
+ <button class="btn btn-world-action" id="new-world-btn" title="Clear everything and start fresh">+ New World</button>
2012
+ <button class="btn btn-world-action" id="load-file-btn" title="Load a .json world file">Load World File</button>
2013
+ <button class="btn btn-world-action" id="clear-rules-btn" title="Clear custom rules only">Clear Rules</button>
2014
+ <button class="btn btn-world-action btn-export" id="export-world-btn" title="Export current world as JSON">Save as World File</button>
2015
+ </div>
2016
+
2017
+ <!-- World Source selector -->
2018
+ <div class="ctrl-section">
2019
+ <h3>World Source</h3>
2020
+ <div class="world-source-tabs">
2021
+ <label class="ws-tab active" data-source="preset">
2022
+ <input type="radio" name="world-source" value="preset" checked>
2023
+ <span class="ws-label">Preset</span>
2024
+ <span class="ws-hint">Demo scenarios</span>
2025
+ </label>
2026
+ <label class="ws-tab" data-source="custom">
2027
+ <input type="radio" name="world-source" value="custom">
2028
+ <span class="ws-label">Custom Rules</span>
2029
+ <span class="ws-hint">Define your world</span>
2030
+ </label>
2031
+ <label class="ws-tab" data-source="upload">
2032
+ <input type="radio" name="world-source" value="upload">
2033
+ <span class="ws-label">World File</span>
2034
+ <span class="ws-hint">JSON / .nv-world</span>
2035
+ </label>
2036
+ </div>
2037
+ </div>
2038
+
2039
+ <!-- SOURCE: Preset -->
2040
+ <div class="world-source-panel" id="source-preset">
2041
+ <div class="ctrl-section">
2042
+ <h3>World</h3>
2043
+ <div class="ctrl-row">
2044
+ <select id="world-select"></select>
2045
+ </div>
2046
+ <div id="world-thesis" class="world-thesis"></div>
2047
+ </div>
2048
+
2049
+ <!-- State variables (dynamic sliders) -->
2050
+ <div class="ctrl-section" id="state-vars-section" style="display:none">
2051
+ <h3>World Rules</h3>
2052
+ <div id="state-vars"></div>
2053
+ </div>
2054
+
2055
+ <!-- Scenario presets (collapsed by default) -->
2056
+ <div class="ctrl-section">
2057
+ <h3 style="cursor:pointer" onclick="var el=document.getElementById('scenario-list');var a=this.querySelector('.arrow');if(el.style.display==='none'){el.style.display='';a.textContent='▼'}else{el.style.display='none';a.textContent='▶'}"><span class="arrow" style="font-size:9px;margin-right:4px">▶</span>Scenarios</h3>
2058
+ <div id="scenario-list" style="display:none"></div>
2059
+ </div>
2060
+ </div>
2061
+
2062
+ <!-- SOURCE: Custom Rules (Define Your World) -->
2063
+ <div class="world-source-panel" id="source-custom" style="display:none">
2064
+ <div class="ctrl-section">
2065
+ <h3>Define Your World</h3>
2066
+ <div class="custom-world-header">
2067
+ <input type="text" class="world-name-input" id="custom-world-name" placeholder="World name (e.g. Marketing Governance)">
2068
+ <textarea class="world-thesis-input" id="custom-world-thesis" placeholder="What is this world about? (thesis)"></textarea>
2069
+ </div>
2070
+ <div class="rule-editor">
2071
+ <div class="rule-editor-label">Type your governance rules:</div>
2072
+ <textarea class="rule-input rule-input-large" id="rule-input" placeholder="No agent may spend more than $10k without approval&#10;All outbound emails must be reviewed&#10;Block deletion of production data&#10;Limit API calls to 100 per minute&#10;Require manager approval for refunds over $500"></textarea>
2073
+ <button class="btn btn-generate-world" id="parse-rules-btn">Generate World</button>
2074
+ <div id="parsed-rules" class="parsed-rules"></div>
2075
+ <div id="rule-status" class="rule-status"></div>
2076
+ <div class="rule-examples">
2077
+ Rule patterns:<br>
2078
+ <code>Block [action]</code> — hard suppression<br>
2079
+ <code>Limit [X] to [N]</code> — cap extremes<br>
2080
+ <code>Require [X] for [Y]</code> — structural constraint<br>
2081
+ <code>Pause [X] for review</code> — human-in-the-loop<br>
2082
+ <code>Allow [X]</code> — explicit permission<br>
2083
+ <code>Monitor [X]</code> — circuit breaker gate
2084
+ </div>
2085
+ </div>
2086
+ </div>
2087
+
2088
+ <!-- Base world (optional) -->
2089
+ <div class="ctrl-section">
2090
+ <h3>Base World (Optional)</h3>
2091
+ <div class="ctrl-row">
2092
+ <select id="custom-base-world">
2093
+ <option value="">None — start from scratch</option>
2094
+ </select>
2095
+ <div style="font-size:10px;color:var(--text-faint);margin-top:4px">Layer your rules on top of a preset world</div>
2096
+ </div>
2097
+ </div>
2098
+ </div>
2099
+
2100
+ <!-- SOURCE: Upload World File -->
2101
+ <div class="world-source-panel" id="source-upload" style="display:none">
2102
+ <div class="ctrl-section">
2103
+ <h3>Load World File</h3>
2104
+ <div class="upload-zone" id="upload-zone">
2105
+ <div class="upload-icon">&#x1F4C4;</div>
2106
+ <div class="upload-label">Drop a .json or .nv-world file here</div>
2107
+ <div class="upload-or">or</div>
2108
+ <button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
2109
+ <input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
2110
+ </div>
2111
+ <div class="upload-paste-section">
2112
+ <div class="rule-editor-label">Or paste world JSON:</div>
2113
+ <textarea class="rule-input rule-input-large" id="world-json-input" placeholder='{&#10; "name": "Marketing Governance",&#10; "thesis": "All marketing actions are governed",&#10; "invariants": [...],&#10; "rules": [...],&#10; "gates": [...]&#10;}'></textarea>
2114
+ </div>
2115
+ <button class="btn btn-load-world" id="load-world-btn">Load into Runtime</button>
2116
+ <div id="upload-status" class="rule-status"></div>
2117
+
2118
+ <!-- Loaded world info -->
2119
+ <div id="loaded-world-info" style="display:none">
2120
+ <div class="loaded-world-card">
2121
+ <div class="lw-name" id="lw-name"></div>
2122
+ <div class="lw-thesis" id="lw-thesis"></div>
2123
+ <div class="lw-stats" id="lw-stats"></div>
2124
+ </div>
2125
+ </div>
2126
+ </div>
2127
+
2128
+ <!-- World file schema reference -->
2129
+ <div class="ctrl-section">
2130
+ <h3>World File Schema</h3>
2131
+ <div class="schema-ref">
2132
+ <div class="schema-item"><code>name</code> — world name</div>
2133
+ <div class="schema-item"><code>thesis</code> — what this world is about</div>
2134
+ <div class="schema-item"><code>rules[]</code> — governance rules (plain English or structured)</div>
2135
+ <div class="schema-item"><code>invariants[]</code> — rules that always hold <code>{id, description}</code></div>
2136
+ <div class="schema-item"><code>gates[]</code> — viability thresholds <code>{id, label, condition, severity}</code></div>
2137
+ <div class="schema-item"><code>state_variables[]</code> — sliders <code>{id, label, type, range, default_value}</code></div>
2138
+ </div>
2139
+ </div>
2140
+ </div>
2141
+
2142
+ <!-- Simulation Engine (demoted, below world source) -->
2143
+ <div class="ctrl-section" style="margin-top:8px">
2144
+ <h3>Engine</h3>
2145
+ <div class="ctrl-row">
2146
+ <select id="engine-select">
2147
+ <option value="nv-sim" selected>NV-SIM (Built-in)</option>
2148
+ </select>
2149
+ </div>
2150
+ <div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
2151
+ </div>
2152
+
2153
+ <!-- Narrative injection -->
2154
+ <div class="ctrl-section">
2155
+ <h3>Narrative Events</h3>
2156
+ <div class="inject-row">
2157
+ <select id="event-select"></select>
2158
+ <input type="number" id="event-round" min="1" max="20" value="3" placeholder="R">
2159
+ </div>
2160
+ <button class="btn btn-add" id="add-event-btn">+ Add Event</button>
2161
+ <div id="inject-list" class="inject-list" style="margin-top:8px"></div>
2162
+ </div>
2163
+
2164
+ <!-- Rounds -->
2165
+ <div class="ctrl-section">
2166
+ <h3>Simulation</h3>
2167
+ <div class="ctrl-row">
2168
+ <div class="ctrl-label">
2169
+ <span>Rounds</span>
2170
+ <span class="val" id="rounds-val">5</span>
2171
+ </div>
2172
+ <input type="range" id="rounds-slider" min="3" max="12" value="5">
2173
+ </div>
2174
+ </div>
2175
+
2176
+ <!-- Run button -->
2177
+ <button class="btn btn-run" id="run-btn">Run Simulation</button>
2178
+
2179
+ <!-- Save as variant -->
2180
+ <div id="save-section" style="margin-top:12px">
2181
+ <button class="btn btn-save" id="save-btn">Save as World Variant</button>
2182
+ <div id="save-form" style="display:none;margin-top:8px">
2183
+ <input type="text" id="variant-name" placeholder="Variant name (e.g. Hormuz Closed + 3x Leverage)" class="save-input">
2184
+ <input type="text" id="variant-desc" placeholder="What does this variant test?" class="save-input" style="margin-top:4px">
2185
+ <div style="display:flex;gap:6px;margin-top:6px">
2186
+ <button class="btn btn-confirm" id="confirm-save-btn">Save</button>
2187
+ <button class="btn btn-cancel" id="cancel-save-btn">Cancel</button>
2188
+ </div>
2189
+ </div>
2190
+ </div>
2191
+
2192
+ <!-- Saved variants -->
2193
+ <div class="ctrl-section" style="margin-top:16px">
2194
+ <h3>Your Worlds</h3>
2195
+ <div id="variant-list"><div style="font-size:11px;color:#333">No saved variants yet</div></div>
2196
+ </div>
2197
+
2198
+ <!-- Integration Quick-Start -->
2199
+ <div class="ctrl-section" style="margin-top:16px">
2200
+ <h3>Govern Any Agent System</h3>
2201
+
2202
+ <div class="arch-callout">
2203
+ <div class="arch-title">Pre-Execution Enforcement</div>
2204
+ <div class="arch-flow">
2205
+ Agent decides → <span class="arch-yes">Governance evaluates</span> → <span class="arch-yes">THEN executes</span>
2206
+ </div>
2207
+ <div class="arch-warn">Governance MUST happen BEFORE execution. Post-execution = audit only, not control.</div>
2208
+ </div>
2209
+
2210
+ <div class="fw-tabs">
2211
+ <button class="fw-tab active" onclick="switchFw('generic',this)">Generic<span class="fw-sub">any agent</span></button>
2212
+ <button class="fw-tab" onclick="switchFw('scienceclaw',this)">ScienceClaw<span class="fw-sub">research</span></button>
2213
+ <button class="fw-tab" onclick="switchFw('mirofish',this)">MiroFish<span class="fw-sub">OASIS</span></button>
2214
+ <button class="fw-tab" onclick="switchFw('langchain',this)">LangChain<span class="fw-sub">tools</span></button>
2215
+ </div>
2216
+
2217
+ <div class="fw-panel active" id="fw-generic">
2218
+ <div class="integrate-section">
2219
+ <h4>Insert before your agent executes</h4>
2220
+ <div class="lang-tabs">
2221
+ <button class="lang-tab active" onclick="switchLang('generic-py',this)">Python</button>
2222
+ <button class="lang-tab" onclick="switchLang('generic-js',this)">JavaScript</button>
2223
+ <button class="lang-tab" onclick="switchLang('generic-curl',this)">cURL</button>
2224
+ </div>
2225
+ <div class="lang-code-panel active" id="lang-generic-py"><div class="integrate-code"><span class="kw">import</span> requests
2226
+
2227
+ <span class="kw">for</span> agent <span class="kw">in</span> agents:
2228
+ raw_action = agent.decide()
2229
+
2230
+ <span class="kw">try</span>:
2231
+ verdict = requests.post(
2232
+ <span class="str">"http://localhost:3456/api/evaluate"</span>,
2233
+ json={<span class="str">"actor"</span>: agent.id,
2234
+ <span class="str">"action"</span>: raw_action,
2235
+ <span class="str">"world"</span>: <span class="str">"trading"</span>},
2236
+ timeout=0.5
2237
+ ).json()
2238
+ <span class="kw">except</span>:
2239
+ verdict = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
2240
+
2241
+ <span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
2242
+ <span class="kw">continue</span> <span class="comment"># skip this action entirely</span>
2243
+ <span class="kw">elif</span> verdict[<span class="str">"decision"</span>] == <span class="str">"MODIFY"</span>:
2244
+ raw_action = verdict[<span class="str">"modified_action"</span>]
2245
+
2246
+ environment.apply(agent, raw_action)</div></div>
2247
+ <div class="lang-code-panel" id="lang-generic-js"><div class="integrate-code"><span class="kw">for</span> (<span class="kw">const</span> agent <span class="kw">of</span> agents) {
2248
+ <span class="kw">let</span> action = agent.decide()
2249
+
2250
+ <span class="kw">const</span> verdict = <span class="kw">await</span> fetch(
2251
+ <span class="str">"http://localhost:3456/api/evaluate"</span>, {
2252
+ method: <span class="str">"POST"</span>,
2253
+ headers: {<span class="str">"Content-Type"</span>: <span class="str">"application/json"</span>},
2254
+ body: JSON.stringify({
2255
+ actor: agent.id, action, world: <span class="str">"trading"</span>
2256
+ })
2257
+ }).then(r => r.json())
2258
+
2259
+ <span class="kw">if</span> (verdict.decision === <span class="str">"BLOCK"</span>) <span class="kw">continue</span>
2260
+ <span class="kw">if</span> (verdict.decision === <span class="str">"MODIFY"</span>)
2261
+ action = verdict.modified_action
2262
+
2263
+ environment.apply(agent, action)
2264
+ }</div></div>
2265
+ <div class="lang-code-panel" id="lang-generic-curl"><div class="integrate-code">curl -X POST http://localhost:3456/api/evaluate \
2266
+ -H <span class="str">"Content-Type: application/json"</span> \
2267
+ -d <span class="str">'{"actor":"agent_1","action":"panic_sell"}'</span>
2268
+
2269
+ <span class="comment"># Returns: {"decision":"BLOCK","reason":"..."}</span>
2270
+ <span class="comment"># Watch it appear in the dashboard →</span></div></div>
2271
+ </div>
2272
+ </div>
2273
+
2274
+ <div class="fw-panel" id="fw-scienceclaw">
2275
+ <div class="integrate-section">
2276
+ <h4>Detect action type, then evaluate</h4>
2277
+ <div style="font-size:10px;color:var(--yellow);margin-bottom:6px;line-height:1.4">
2278
+ Don't hardcode <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">action = "post"</code> — detect intent from agent output so rules can differentiate.
2279
+ </div>
2280
+ <div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate, detect_action_type
2281
+
2282
+ <span class="comment"># Step 1: Agent generates content</span>
2283
+ results = generator.run_pubmed_search(topic)
2284
+ output = generator.generate_content(topic, results)
2285
+
2286
+ <span class="comment"># Step 2: Detect what kind of action this is</span>
2287
+ action = detect_action_type(output[<span class="str">"content"</span>])
2288
+ <span class="comment"># → "analyze", "publish", "cite", "recommend"</span>
2289
+
2290
+ <span class="comment"># Step 3: Evaluate BEFORE executing</span>
2291
+ verdict = evaluate(
2292
+ actor=<span class="str">"Harry"</span>,
2293
+ action=action,
2294
+ world=<span class="str">"research"</span>
2295
+ )
2296
+
2297
+ <span class="comment"># Step 4: Only execute if allowed</span>
2298
+ <span class="kw">if</span> verdict[<span class="str">"decision"</span>] != <span class="str">"BLOCK"</span>:
2299
+ generator.post_to_infinite(output)</div>
2300
+ <div style="font-size:9px;color:var(--text-muted);margin-top:6px;line-height:1.5">
2301
+ See <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">bridge/scienceclaw_governed.py</code> for full example
2302
+ </div>
2303
+ </div>
2304
+ </div>
2305
+
2306
+ <div class="fw-panel" id="fw-mirofish">
2307
+ <div class="integrate-section">
2308
+ <h4>Replace the dict comprehension</h4>
2309
+ <div style="font-size:10px;color:var(--red);margin-bottom:6px;line-height:1.4">
2310
+ MiroFish executes inside <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">env.step()</code> — governance MUST happen before that call.
2311
+ </div>
2312
+ <div class="integrate-code" style="margin-bottom:6px;border-left:3px solid var(--red);opacity:0.6"><span class="comment"># ❌ BEFORE (opaque — no governance possible)</span>
2313
+ actions = {agent: LLMAction()
2314
+ <span class="kw">for</span> _, agent <span class="kw">in</span> active_agents}
2315
+ <span class="kw">await</span> result.env.step(actions)</div>
2316
+ <div class="integrate-code" style="border-left:3px solid var(--green)"><span class="comment"># ✅ AFTER (expand the loop, insert governance)</span>
2317
+ <span class="kw">import</span> requests
2318
+
2319
+ actions = {}
2320
+ <span class="kw">for</span> _, agent <span class="kw">in</span> active_agents:
2321
+ raw_action = LLMAction()
2322
+
2323
+ <span class="kw">try</span>:
2324
+ verdict = requests.post(
2325
+ <span class="str">"http://localhost:3456/api/evaluate"</span>,
2326
+ json={
2327
+ <span class="str">"actor"</span>: getattr(agent, <span class="str">"id"</span>, str(agent)),
2328
+ <span class="str">"action"</span>: str(raw_action),
2329
+ <span class="str">"world"</span>: <span class="str">"social_media"</span>
2330
+ },
2331
+ timeout=0.5
2332
+ ).json()
2333
+ <span class="kw">except</span>:
2334
+ verdict = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
2335
+
2336
+ <span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
2337
+ <span class="kw">continue</span>
2338
+ <span class="kw">elif</span> verdict[<span class="str">"decision"</span>] == <span class="str">"MODIFY"</span>:
2339
+ raw_action = verdict.get(<span class="str">"modified_action"</span>, raw_action)
2340
+
2341
+ actions[agent] = raw_action
2342
+
2343
+ <span class="kw">await</span> result.env.step(actions)</div>
2344
+ <div style="font-size:9px;color:var(--text-muted);margin-top:6px;line-height:1.5">
2345
+ Apply to all 3 scripts: <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_twitter_simulation.py</code>
2346
+ <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_reddit_simulation.py</code>
2347
+ <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_parallel_simulation.py</code>
2348
+ </div>
2349
+ </div>
2350
+ </div>
2351
+
2352
+ <div class="fw-panel" id="fw-langchain">
2353
+ <div class="integrate-section">
2354
+ <h4>Wrap your tool or agent executor</h4>
2355
+ <div class="integrate-code"><span class="kw">import</span> requests
2356
+ <span class="kw">from</span> langchain.tools <span class="kw">import</span> tool
2357
+
2358
+ <span class="kw">def</span> governed(fn, world=<span class="str">"trading"</span>):
2359
+ <span class="kw">def</span> wrapper(*args, **kwargs):
2360
+ action = fn.__name__
2361
+ <span class="kw">try</span>:
2362
+ v = requests.post(
2363
+ <span class="str">"http://localhost:3456/api/evaluate"</span>,
2364
+ json={<span class="str">"actor"</span>: <span class="str">"langchain"</span>,
2365
+ <span class="str">"action"</span>: action,
2366
+ <span class="str">"world"</span>: world},
2367
+ timeout=0.5
2368
+ ).json()
2369
+ <span class="kw">except</span>:
2370
+ v = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
2371
+ <span class="kw">if</span> v[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
2372
+ <span class="kw">return</span> f<span class="str">"Blocked: {v['reason']}"</span>
2373
+ <span class="kw">return</span> fn(*args, **kwargs)
2374
+ <span class="kw">return</span> wrapper
2375
+
2376
+ <span class="comment"># Usage: wrap any tool</span>
2377
+ @tool
2378
+ @governed
2379
+ <span class="kw">def</span> execute_trade(ticker, amount):
2380
+ ...</div>
2381
+ </div>
2382
+ </div>
2383
+
2384
+ <div style="margin-top:8px">
2385
+ <div class="integrate-hint">
2386
+ <span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> stopped
2387
+ <span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> adjusted
2388
+ <span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
2389
+ </div>
2390
+ <div style="font-size:10px;color:var(--text-faint);margin-top:8px;border-top:1px solid var(--border);padding-top:6px">
2391
+ The world file controls everything. Same rules govern simulated and live systems.
2392
+ </div>
2393
+ </div>
2394
+ </div>
2395
+
2396
+ <!-- Session Report Panel -->
2397
+ <div class="ctrl-section" id="session-panel">
2398
+ <h3 class="ctrl-label">SESSION</h3>
2399
+ <div class="metric-grid" style="grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
2400
+ <div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
2401
+ <div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
2402
+ <div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
2403
+ <div class="metric-box"><div class="value" id="s-allowed" style="color:#4ade80">0</div><div class="label">Allowed</div></div>
2404
+ </div>
2405
+ <div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
2406
+ <div style="display:flex;gap:6px;flex-wrap:wrap">
2407
+ <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>
2408
+ <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>
2409
+ <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>
2410
+ </div>
2411
+ <div id="session-history" style="margin-top:6px;font-size:10px;color:#666"></div>
2412
+ </div>
2413
+ </div>
2414
+ </div>
2415
+
2416
+ <!-- RIGHT: VIEWER -->
2417
+ <div class="viewer">
2418
+
2419
+ <!-- LAYER 1: OUTCOME -->
2420
+ <div class="outcome-panel" id="outcome-panel">
2421
+ <div class="outcome-statement" id="outcome-statement">
2422
+ <span class="outcome-empty">Run a simulation or connect an agent to see outcomes</span>
2423
+ </div>
2424
+ <div class="confidence-grid" id="confidence-grid" style="display:none">
2425
+ <div class="confidence-card">
2426
+ <div class="cc-label">Conclusion Strength</div>
2427
+ <div class="cc-value" id="cc-strength">--</div>
2428
+ </div>
2429
+ <div class="confidence-card">
2430
+ <div class="cc-label">Evidence Quality</div>
2431
+ <div class="cc-value" id="cc-evidence">--</div>
2432
+ </div>
2433
+ <div class="confidence-card">
2434
+ <div class="cc-label">Risk of Error</div>
2435
+ <div class="cc-value" id="cc-risk">--</div>
2436
+ </div>
2437
+ </div>
2438
+ <div class="outcome-context" id="outcome-context"></div>
2439
+ </div>
2440
+
2441
+ <!-- LAYER 2: BEHAVIOR -->
2442
+ <div class="behavior-panel" id="behavior-panel-v2">
2443
+ <h2>What Agents Did</h2>
2444
+ <div class="behavior-shifts" id="behavior-shifts">
2445
+ <div class="behavior-empty">Waiting for agent actions</div>
2446
+ </div>
2447
+ <div class="activity-toggle" id="activity-toggle" onclick="document.getElementById('activity-timeline').classList.toggle('open');this.querySelector('.arrow').textContent=document.getElementById('activity-timeline').classList.contains('open')?'▼':'▶'" style="display:none">
2448
+ <span class="arrow">▶</span> See activity timeline
2449
+ </div>
2450
+ <div class="activity-timeline" id="activity-timeline"></div>
2451
+ </div>
2452
+
2453
+ <!-- LAYER 3: WHY -->
2454
+ <div class="why-panel" id="why-panel">
2455
+ <h2>Why This Happened</h2>
2456
+ <div id="why-content">
2457
+ <div class="why-empty">Causation analysis appears after agent actions are evaluated</div>
2458
+ </div>
2459
+ </div>
2460
+
2461
+ <!-- LAYER 4: EXPORT -->
2462
+ <div class="export-bar">
2463
+ <button class="export-btn" onclick="exportPDF()">Download PDF</button>
2464
+ <button class="export-btn" onclick="exportCSV()">Export CSV</button>
2465
+ <button class="export-btn" onclick="copyShareSummary()">Copy Summary</button>
2466
+ <button class="export-btn audit-btn" onclick="openAudit()">Audit Trail</button>
2467
+ </div>
2468
+
2469
+ <!-- Hidden elements for data tracking (not displayed) -->
2470
+ <div style="display:none">
2471
+ <span id="m-stability">--</span>
2472
+ <span id="m-volatility">--</span>
2473
+ <span id="m-round">--</span>
2474
+ <span id="m-interventions">0</span>
2475
+ <span id="trace-source"></span>
2476
+ <div id="agents"></div>
2477
+ <div id="active-invariants"></div>
2478
+ <div id="log"></div>
2479
+ </div>
2480
+ </div>
2481
+
2482
+ <!-- LAYER 5: AUDIT OVERLAY (separate from dashboard) -->
2483
+ <div class="audit-overlay" id="audit-overlay" onclick="if(event.target===this)closeAudit()">
2484
+ <div class="audit-modal">
2485
+ <div class="audit-header">
2486
+ <h2>Audit Trail</h2>
2487
+ <button class="audit-close" onclick="closeAudit()">Close</button>
2488
+ </div>
2489
+ <div class="audit-section" id="audit-rules">
2490
+ <h3>Active Rules</h3>
2491
+ <div id="audit-rules-content"></div>
2492
+ </div>
2493
+ <div class="audit-section" id="audit-verdicts">
2494
+ <h3>Verdict Log</h3>
2495
+ <div id="audit-verdicts-content"></div>
2496
+ </div>
2497
+ <div class="audit-section" id="audit-trace">
2498
+ <h3>Detailed Trace</h3>
2499
+ <div id="audit-trace-content"></div>
2500
+ </div>
2501
+ </div>
2502
+ </div>
2503
+ </div>
2504
+
2505
+ <script>
2506
+ // ============================================
2507
+ // LANGUAGE & FRAMEWORK TABS
2508
+ // ============================================
2509
+ function switchLang(lang, btn) {
2510
+ const container = btn.closest('.integrate-section') || document;
2511
+ container.querySelectorAll('.lang-tab').forEach(t => t.classList.remove('active'));
2512
+ container.querySelectorAll('.lang-code-panel').forEach(p => p.classList.remove('active'));
2513
+ btn.classList.add('active');
2514
+ const panel = document.getElementById('lang-' + lang);
2515
+ if (panel) panel.classList.add('active');
2516
+ }
2517
+
2518
+ function switchFw(fw, btn) {
2519
+ document.querySelectorAll('.fw-tab').forEach(t => t.classList.remove('active'));
2520
+ document.querySelectorAll('.fw-panel').forEach(p => p.classList.remove('active'));
2521
+ btn.classList.add('active');
2522
+ const panel = document.getElementById('fw-' + fw);
2523
+ if (panel) panel.classList.add('active');
2524
+ }
2525
+
2526
+ // ============================================
2527
+ // OUTCOME INTELLIGENCE LAYER
2528
+ // ============================================
2529
+
2530
+ function generateOutcome(stability, volatility, shiftData, worldThesis) {
2531
+ var st = stability || 0;
2532
+ var vol = volatility || 0;
2533
+ var blockRatio = shiftData.total > 0 ? shiftData.blocks / shiftData.total : 0;
2534
+ var thesis = worldThesis || '';
2535
+
2536
+ // Determine outcome direction + dominant behavior (state + what agents did)
2537
+ var direction = '';
2538
+ var behavior = '';
2539
+ if (st > 0.7 && vol < 0.3) {
2540
+ direction = 'stabilized';
2541
+ behavior = 'agents shifted toward safer positions';
2542
+ } else if (st > 0.7 && vol >= 0.3) {
2543
+ direction = 'held under pressure';
2544
+ behavior = 'agents maintained cautious strategies despite volatility';
2545
+ } else if (st > 0.4 && vol < 0.5) {
2546
+ direction = 'partially converged';
2547
+ behavior = 'agents split between aggressive and conservative approaches';
2548
+ } else if (st <= 0.4 && vol >= 0.5) {
2549
+ direction = 'fragmented';
2550
+ behavior = 'agents competed with conflicting strategies';
2551
+ } else if (st <= 0.4) {
2552
+ direction = 'remains uncertain';
2553
+ behavior = 'agents failed to find a dominant strategy';
2554
+ } else {
2555
+ direction = 'showed mixed results';
2556
+ behavior = 'agents oscillated between risk-taking and caution';
2557
+ }
2558
+
2559
+ // Enrich with world context if available
2560
+ if (thesis) {
2561
+ var nouns = thesis.match(/\b(market|research|supply|trade|financial|climate|regulatory|investment|innovation|security)\b/i);
2562
+ if (nouns) {
2563
+ var subject = nouns[1].charAt(0).toUpperCase() + nouns[1].slice(1).toLowerCase();
2564
+ return subject + ' ' + direction + ' as ' + behavior;
2565
+ }
2566
+ }
2567
+
2568
+ // Build behavior-enriched statement — always state + dominant behavior
2569
+ if (blockRatio > 0.4 && st > 0.6) return 'System ' + direction + ' after agents abandoned high-risk strategies';
2570
+ if (blockRatio > 0.2) return 'Outcome ' + direction + ' as ' + behavior;
2571
+ if (shiftData.patterns && shiftData.patterns.length > 0) return 'Outcome ' + direction + ' — ' + behavior;
2572
+ return 'Outcome ' + direction + ' as ' + behavior;
2573
+ }
2574
+
2575
+ function computeConfidence(stability, volatility, interventions, total) {
2576
+ var interventionRate = total > 0 ? interventions / total : 0;
2577
+ var score = stability * 0.5 + (1 - volatility) * 0.3 + (1 - interventionRate) * 0.2;
2578
+
2579
+ var strength, evidence, risk, strengthCls, evidenceCls, riskCls;
2580
+
2581
+ if (score > 0.75) {
2582
+ strength = 'Strong'; strengthCls = 'good';
2583
+ evidence = 'Solid'; evidenceCls = 'good';
2584
+ risk = 'Low'; riskCls = 'good';
2585
+ } else if (score > 0.55) {
2586
+ strength = 'Moderate'; strengthCls = 'warn';
2587
+ evidence = stability > 0.6 ? 'Solid' : 'Mixed'; evidenceCls = stability > 0.6 ? 'good' : 'warn';
2588
+ risk = volatility > 0.4 ? 'Elevated' : 'Moderate'; riskCls = volatility > 0.4 ? 'warn' : 'warn';
2589
+ } else if (score > 0.35) {
2590
+ strength = 'Moderate'; strengthCls = 'warn';
2591
+ evidence = 'Mixed'; evidenceCls = 'warn';
2592
+ risk = 'Elevated'; riskCls = 'bad';
2593
+ } else {
2594
+ strength = 'Weak'; strengthCls = 'bad';
2595
+ evidence = 'Thin'; evidenceCls = 'bad';
2596
+ risk = 'High'; riskCls = 'bad';
2597
+ }
2598
+
2599
+ return { score: score, strength: strength, strengthCls: strengthCls, evidence: evidence, evidenceCls: evidenceCls, risk: risk, riskCls: riskCls };
2600
+ }
2601
+
2602
+ function translateBehaviorNarrative(reaction, verdict) {
2603
+ // Convert verdict+action into before → after narrative — no system words
2604
+ var status = verdict ? verdict.status : 'ALLOW';
2605
+ var action = reaction || 'acted';
2606
+ if (status === 'BLOCK') {
2607
+ return 'abandoned ' + action + ' and switched to a safer strategy';
2608
+ }
2609
+ if (status === 'MODIFY' || status === 'PAUSE') {
2610
+ return 'scaled back ' + action + ' after early resistance';
2611
+ }
2612
+ return action;
2613
+ }
2614
+
2615
+ function generateBehaviorShifts(shiftData, bLog) {
2616
+ // Quantified behavioral shifts in human language
2617
+ var sentences = [];
2618
+ var total = shiftData.total || bLog.length || 0;
2619
+ if (total === 0) return sentences;
2620
+
2621
+ // Group by what agents actually did — always anchor in before → after
2622
+ var keys = Object.keys(shiftData.shifts || {}).sort(function(a, b) { return shiftData.shifts[b] - shiftData.shifts[a]; });
2623
+ if (keys.length > 0 && total > 0) {
2624
+ keys.slice(0, 3).forEach(function(k) {
2625
+ var parts = k.split(': ');
2626
+ var action = parts[1] || parts[0];
2627
+ var count = shiftData.shifts[k];
2628
+ var pct = Math.round((count / total) * 100);
2629
+ // before → after: what they tried → what they did instead
2630
+ if (parts[0] === 'BLOCK') {
2631
+ sentences.push(pct + '% of agents (' + count + ') shifted from ' + action + ' to conservative strategies');
2632
+ } else {
2633
+ sentences.push(pct + '% of agents (' + count + ') reduced ' + action + ' after initial attempts failed');
2634
+ }
2635
+ });
2636
+ }
2637
+
2638
+ // Fallback from behavioral log — still before → after
2639
+ if (sentences.length === 0 && bLog.length > 0) {
2640
+ var blocked = bLog.filter(function(e) { return e.status === 'BLOCK'; }).length;
2641
+ var modified = bLog.filter(function(e) { return e.status === 'MODIFY' || e.status === 'PAUSE'; }).length;
2642
+ var proceeded = bLog.length - blocked - modified;
2643
+
2644
+ if (blocked > 0) {
2645
+ var bPct = Math.round((blocked / bLog.length) * 100);
2646
+ sentences.push(bPct + '% of agents (' + blocked + ') shifted from aggressive to conservative strategies');
2647
+ }
2648
+ if (modified > 0) {
2649
+ var mPct = Math.round((modified / bLog.length) * 100);
2650
+ sentences.push(mPct + '% of agents (' + modified + ') reduced position size after initial attempts failed');
2651
+ }
2652
+ if (proceeded > 0 && (blocked > 0 || modified > 0)) {
2653
+ var pPct = Math.round((proceeded / bLog.length) * 100);
2654
+ sentences.push(pPct + '% of agents (' + proceeded + ') maintained their original strategy throughout');
2655
+ }
2656
+ }
2657
+
2658
+ // Volatility insight — before → after framing
2659
+ if (shiftData.governedVol < shiftData.baselineVol && shiftData.baselineVol > 0) {
2660
+ var volDrop = Math.round((1 - shiftData.governedVol / shiftData.baselineVol) * 100);
2661
+ sentences.push('Uncertainty dropped ' + volDrop + '% as agents moved from exploration to caution');
2662
+ }
2663
+
2664
+ // Add emergent pattern insights
2665
+ if (shiftData.patterns && shiftData.patterns.length > 0) {
2666
+ shiftData.patterns.slice(0, 2).forEach(function(p) {
2667
+ sentences.push(p);
2668
+ });
2669
+ }
2670
+
2671
+ return sentences;
2672
+ }
2673
+
2674
+ function generateCausation(shiftData, bLog) {
2675
+ // Pure causation — why behavior changed — no governance terminology
2676
+ var causes = [];
2677
+ var total = shiftData.total || bLog.length || 0;
2678
+ if (total === 0) return causes;
2679
+
2680
+ var blockRatio = shiftData.blocks / total;
2681
+
2682
+ // No system words (feedback, evaluation, validation, review)
2683
+ // Only agent experience (failed attempts, uncertainty, risk, delay)
2684
+ if (blockRatio > 0.4) {
2685
+ causes.push('Early aggressive attempts failed, forcing agents to rethink their strategy');
2686
+ } else if (blockRatio > 0.1) {
2687
+ causes.push('Some agents hit unexpected resistance and pulled back');
2688
+ }
2689
+
2690
+ if (shiftData.governedVol < shiftData.baselineVol) {
2691
+ causes.push('Uncertainty dropped as agents stopped experimenting and committed to safer positions');
2692
+ }
2693
+
2694
+ if (shiftData.patterns && shiftData.patterns.length > 0) {
2695
+ if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('hold') || p.toLowerCase().includes('caution'); })) {
2696
+ causes.push('Risk became too visible — agents chose to wait rather than act');
2697
+ }
2698
+ if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('coordination') || p.toLowerCase().includes('coordinated'); })) {
2699
+ causes.push('Agents independently converged on similar strategies under shared pressure');
2700
+ }
2701
+ }
2702
+
2703
+ // Detect adaptation pattern from behavior log
2704
+ if (bLog.length >= 3) {
2705
+ var recent = bLog.slice(-6);
2706
+ var earlyBlocks = recent.slice(0, 3).filter(function(e) { return e.status === 'BLOCK'; }).length;
2707
+ var lateAllows = recent.slice(-3).filter(function(e) { return e.status === 'ALLOW'; }).length;
2708
+ if (earlyBlocks >= 2 && lateAllows >= 2) {
2709
+ causes.push('Agents became more cautious after early attempts failed');
2710
+ }
2711
+ }
2712
+
2713
+ if (causes.length === 0) {
2714
+ causes.push('Agents held steady — no major shifts in strategy throughout the simulation');
2715
+ }
2716
+
2717
+ return causes;
2718
+ }
2719
+
2720
+ // ============================================
2721
+ // STATE
2722
+ // ============================================
2723
+ let worlds = [];
2724
+ let scenarios = {};
2725
+ let narratives = {};
2726
+ let currentWorld = null;
2727
+ let injectedEvents = [];
2728
+ let totalInterventions = 0;
2729
+ let totalActions = 0;
2730
+ let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
2731
+ let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
2732
+ let behaviorLog = []; // { agent, action, status, reason, ts }
2733
+ let latestStability = 0;
2734
+ let latestVolatility = 0;
2735
+
2736
+ const statusEl = document.getElementById('status');
2737
+ const worldSelect = document.getElementById('world-select');
2738
+ const stateVarsSection = document.getElementById('state-vars-section');
2739
+ const stateVarsEl = document.getElementById('state-vars');
2740
+ const scenarioListEl = document.getElementById('scenario-list');
2741
+ const eventSelect = document.getElementById('event-select');
2742
+ const eventRoundInput = document.getElementById('event-round');
2743
+ const injectListEl = document.getElementById('inject-list');
2744
+ const roundsSlider = document.getElementById('rounds-slider');
2745
+ const roundsVal = document.getElementById('rounds-val');
2746
+ const runBtn = document.getElementById('run-btn');
2747
+ const agentsEl = document.getElementById('agents');
2748
+ const logEl = document.getElementById('log');
2749
+ const activeInvEl = document.getElementById('active-invariants');
2750
+ const engineSelect = document.getElementById('engine-select');
2751
+ const engineStatusEl = document.getElementById('engine-status');
2752
+ const traceSourceEl = document.getElementById('trace-source');
2753
+ const outcomeStatementEl = document.getElementById('outcome-statement');
2754
+ const confidenceGridEl = document.getElementById('confidence-grid');
2755
+ const outcomeContextEl = document.getElementById('outcome-context');
2756
+ const behaviorShiftsEl = document.getElementById('behavior-shifts');
2757
+ const activityTimelineEl = document.getElementById('activity-timeline');
2758
+ const activityToggleEl = document.getElementById('activity-toggle');
2759
+ const whyContentEl = document.getElementById('why-content');
2760
+
2761
+ // Audit + Export
2762
+ function openAudit() {
2763
+ // Populate audit content from current data
2764
+ var rulesEl = document.getElementById('audit-rules-content');
2765
+ rulesEl.innerHTML = activeInvEl.innerHTML || '<div style="font-size:11px;color:#666">No rules loaded</div>';
2766
+
2767
+ var verdictsEl = document.getElementById('audit-verdicts-content');
2768
+ var vHtml = '';
2769
+ behaviorLog.forEach(function(e) {
2770
+ vHtml += '<div style="font-size:10px;padding:2px 0;display:flex;gap:8px;border-bottom:1px solid var(--border)">' +
2771
+ '<span style="color:var(--blue);min-width:100px">' + e.agent + '</span>' +
2772
+ '<span style="flex:1;color:var(--text-secondary)">' + e.action + '</span>' +
2773
+ '<span style="font-size:9px;font-weight:600;padding:0 4px;border-radius:2px;' +
2774
+ (e.status === 'BLOCK' ? 'background:var(--red-bg);color:var(--red)' : e.status === 'ALLOW' ? 'background:var(--green-bg);color:var(--green)' : 'background:var(--yellow-bg);color:var(--yellow)') + '">' + e.status + '</span>' +
2775
+ (e.reason ? '<span style="font-size:9px;color:var(--text-faint)">' + e.reason + '</span>' : '') +
2776
+ '</div>';
2777
+ });
2778
+ verdictsEl.innerHTML = vHtml || '<div style="font-size:11px;color:#666">No verdict data</div>';
2779
+
2780
+ var traceEl = document.getElementById('audit-trace-content');
2781
+ traceEl.innerHTML = logEl.innerHTML || '<div style="font-size:11px;color:#666">No trace data</div>';
2782
+
2783
+ document.getElementById('audit-overlay').classList.add('open');
2784
+ }
2785
+
2786
+ function closeAudit() {
2787
+ document.getElementById('audit-overlay').classList.remove('open');
2788
+ }
2789
+
2790
+ function exportPDF() {
2791
+ window.print();
2792
+ }
2793
+
2794
+ function exportCSV() {
2795
+ if (behaviorLog.length === 0) { alert('No data to export yet'); return; }
2796
+ var csv = 'Agent,Action,Behavior,Round\\n';
2797
+ behaviorLog.forEach(function(e) {
2798
+ var behavior = translateBehaviorNarrative(e.action, { status: e.status });
2799
+ csv += '"' + e.agent + '","' + e.action + '","' + behavior + '","' + (e.round || '') + '"\\n';
2800
+ });
2801
+ var blob = new Blob([csv], { type: 'text/csv' });
2802
+ var url = URL.createObjectURL(blob);
2803
+ var a = document.createElement('a');
2804
+ a.href = url;
2805
+ a.download = 'simulation-behavior-' + new Date().toISOString().slice(0, 10) + '.csv';
2806
+ a.click();
2807
+ URL.revokeObjectURL(url);
2808
+ }
2809
+
2810
+ function copyShareSummary() {
2811
+ var outcome = outcomeStatementEl.textContent;
2812
+ var shifts = [];
2813
+ behaviorShiftsEl.querySelectorAll('.behavior-shift-item').forEach(function(el) { shifts.push('- ' + el.textContent); });
2814
+ var causes = [];
2815
+ whyContentEl.querySelectorAll('.why-item').forEach(function(el) { causes.push('- ' + el.textContent); });
2816
+
2817
+ var text = 'OUTCOME: ' + outcome + '\\n\\n';
2818
+ if (shifts.length) text += 'BEHAVIOR:\\n' + shifts.join('\\n') + '\\n\\n';
2819
+ if (causes.length) text += 'WHY:\\n' + causes.join('\\n') + '\\n';
2820
+ text += '\\nGenerated by NeuroVerse Simulations';
2821
+
2822
+ navigator.clipboard.writeText(text).then(function() {
2823
+ alert('Summary copied to clipboard');
2824
+ });
2825
+ }
2826
+
2827
+ // ============================================
2828
+ // INIT — Load worlds, scenarios, narratives, adapters
2829
+ // ============================================
2830
+ async function init() {
2831
+ const [wRes, sRes, nRes, aRes] = await Promise.all([
2832
+ fetch('/api/worlds').then(r => r.json()),
2833
+ fetch('/api/scenarios').then(r => r.json()),
2834
+ fetch('/api/narratives').then(r => r.json()),
2835
+ fetch('/api/adapters').then(r => r.json()).catch(() => ({ adapters: [] })),
2836
+ ]);
2837
+
2838
+ worlds = wRes.worlds;
2839
+ scenarios = sRes.scenarios;
2840
+ narratives = nRes.narratives;
2841
+
2842
+ // Populate engine selector with live adapters
2843
+ (aRes.adapters || []).forEach(function(a) {
2844
+ const opt = document.createElement('option');
2845
+ opt.value = a.id;
2846
+ opt.textContent = a.label;
2847
+ engineSelect.appendChild(opt);
2848
+ });
2849
+
2850
+ // Populate world select
2851
+ worlds.forEach(w => {
2852
+ const opt = document.createElement('option');
2853
+ opt.value = w.id;
2854
+ opt.textContent = w.title;
2855
+ worldSelect.appendChild(opt);
2856
+ });
2857
+
2858
+ // Populate event select
2859
+ Object.entries(narratives).forEach(([id, ev]) => {
2860
+ const opt = document.createElement('option');
2861
+ opt.value = id;
2862
+ opt.textContent = ev.headline.slice(0, 40);
2863
+ eventSelect.appendChild(opt);
2864
+ });
2865
+
2866
+ // Populate scenario presets
2867
+ Object.entries(scenarios).forEach(([id, s]) => {
2868
+ const btn = document.createElement('button');
2869
+ btn.className = 'scenario-btn';
2870
+ btn.innerHTML = '<div class="stitle">' + s.title + '</div><div class="sdesc">' + s.description + '</div>';
2871
+ btn.onclick = () => loadScenario(id, s);
2872
+ scenarioListEl.appendChild(btn);
2873
+ });
2874
+
2875
+ // Select first world
2876
+ if (worlds.length > 0) selectWorld(worlds[0].id);
2877
+
2878
+ // Load saved variants
2879
+ await loadVariants();
2880
+
2881
+ // Populate base world selector for custom rules mode
2882
+ populateBaseWorldSelect();
2883
+
2884
+ // Connect SSE
2885
+ connectSSE();
2886
+ }
2887
+
2888
+ function selectWorld(worldId) {
2889
+ currentWorld = worlds.find(w => w.id === worldId);
2890
+ if (!currentWorld) return;
2891
+
2892
+ worldSelect.value = worldId;
2893
+ document.getElementById('world-thesis').textContent = '"' + currentWorld.thesis + '"';
2894
+
2895
+ // Render state variable controls
2896
+ if (currentWorld.stateVariables && currentWorld.stateVariables.length > 0) {
2897
+ stateVarsSection.style.display = '';
2898
+ stateVarsEl.innerHTML = '';
2899
+ currentWorld.stateVariables.forEach(sv => {
2900
+ const row = document.createElement('div');
2901
+ row.className = 'ctrl-row';
2902
+
2903
+ if (sv.type === 'number' && sv.range) {
2904
+ const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
2905
+ row.innerHTML =
2906
+ '<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
2907
+ '<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 + '">';
2908
+ stateVarsEl.appendChild(row);
2909
+ const slider = row.querySelector('input');
2910
+ slider.addEventListener('input', () => {
2911
+ document.getElementById('sv-val-' + sv.id).textContent = slider.value;
2912
+ });
2913
+ } else if (sv.type === 'enum' && sv.enum_values) {
2914
+ row.innerHTML =
2915
+ '<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
2916
+ '<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
2917
+ sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
2918
+ '</select>';
2919
+ stateVarsEl.appendChild(row);
2920
+ } else if (sv.type === 'boolean') {
2921
+ row.innerHTML =
2922
+ '<div class="toggle-row">' +
2923
+ '<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
2924
+ '<span class="toggle-label">' + sv.label + '</span>' +
2925
+ '</div>';
2926
+ stateVarsEl.appendChild(row);
2927
+ const toggle = row.querySelector('.toggle');
2928
+ toggle.addEventListener('click', () => {
2929
+ toggle.classList.toggle('on');
2930
+ });
2931
+ }
2932
+ });
2933
+ } else {
2934
+ stateVarsSection.style.display = 'none';
2935
+ }
2936
+
2937
+ // Show invariants
2938
+ activeInvEl.innerHTML = currentWorld.invariants.map(inv =>
2939
+ '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
2940
+ ).join('') + (currentWorld.gates || []).map(g =>
2941
+ '<div class="inv-item" style="color:' + (g.severity === 'critical' ? '#f87171' : '#fbbf24') + '">[' + g.id + '] ' + g.label + '</div>'
2942
+ ).join('');
2943
+ }
2944
+
2945
+ function loadScenario(id, scenario) {
2946
+ // Set world
2947
+ selectWorld(scenario.world);
2948
+ // Set events
2949
+ injectedEvents = scenario.events.slice();
2950
+ renderInjectedEvents();
2951
+ // Set rounds
2952
+ const r = scenario.rounds || 5;
2953
+ roundsSlider.value = Math.min(r, 12);
2954
+ roundsVal.textContent = Math.min(r, 12);
2955
+ }
2956
+
2957
+ // ============================================
2958
+ // NARRATIVE EVENT INJECTION
2959
+ // ============================================
2960
+ document.getElementById('add-event-btn').addEventListener('click', () => {
2961
+ const eventId = eventSelect.value;
2962
+ const round = parseInt(eventRoundInput.value);
2963
+ if (!eventId || isNaN(round)) return;
2964
+ injectedEvents.push(eventId + '@' + round);
2965
+ renderInjectedEvents();
2966
+ });
2967
+
2968
+ function renderInjectedEvents() {
2969
+ injectListEl.innerHTML = injectedEvents.map((ev, i) =>
2970
+ '<div class="inject-item"><span>' + ev + '</span><span class="remove" data-idx="' + i + '">x</span></div>'
2971
+ ).join('');
2972
+ injectListEl.querySelectorAll('.remove').forEach(el => {
2973
+ el.addEventListener('click', () => {
2974
+ injectedEvents.splice(parseInt(el.dataset.idx), 1);
2975
+ renderInjectedEvents();
2976
+ });
2977
+ });
2978
+ }
2979
+
2980
+ // ============================================
2981
+ // WORLD SELECT
2982
+ // ============================================
2983
+ worldSelect.addEventListener('change', () => selectWorld(worldSelect.value));
2984
+ roundsSlider.addEventListener('input', () => { roundsVal.textContent = roundsSlider.value; });
2985
+
2986
+ // ============================================
2987
+ // RUN SIMULATION
2988
+ // ============================================
2989
+ runBtn.addEventListener('click', async () => {
2990
+ if (!currentWorld) return;
2991
+ runBtn.disabled = true;
2992
+ runBtn.textContent = 'Running...';
2993
+
2994
+ // Reset viewer state
2995
+ totalInterventions = 0;
2996
+ agentsEl.innerHTML = '';
2997
+ logEl.innerHTML = '';
2998
+ document.getElementById('m-stability').textContent = '--';
2999
+ document.getElementById('m-volatility').textContent = '--';
3000
+ document.getElementById('m-round').textContent = '--';
3001
+ document.getElementById('m-interventions').textContent = '0';
3002
+
3003
+ // Gather state overrides
3004
+ const stateOverrides = {};
3005
+ if (currentWorld.stateVariables) {
3006
+ currentWorld.stateVariables.forEach(sv => {
3007
+ const el = document.getElementById('sv-' + sv.id);
3008
+ if (!el) return;
3009
+ if (sv.type === 'number') stateOverrides[sv.id] = parseFloat(el.value);
3010
+ else if (sv.type === 'boolean') stateOverrides[sv.id] = el.classList.contains('on');
3011
+ else stateOverrides[sv.id] = el.value;
3012
+ });
3013
+ }
3014
+
3015
+ const selectedEngine = engineSelect.value;
3016
+
3017
+ try {
3018
+ if (selectedEngine === 'nv-sim') {
3019
+ // Built-in simulation
3020
+ const config = {
3021
+ worldId: currentWorld.id,
3022
+ stateOverrides,
3023
+ injectEvents: injectedEvents.length > 0 ? injectedEvents : undefined,
3024
+ rounds: parseInt(roundsSlider.value),
3025
+ };
3026
+ await fetch('/api/run-sim', {
3027
+ method: 'POST',
3028
+ headers: { 'Content-Type': 'application/json' },
3029
+ body: JSON.stringify(config),
3030
+ });
3031
+ } else {
3032
+ // Live adapter (external simulator)
3033
+ const payload = {
3034
+ adapterId: selectedEngine,
3035
+ worldId: currentWorld.id,
3036
+ stateOverrides,
3037
+ };
3038
+ await fetch('/api/run-live', {
3039
+ method: 'POST',
3040
+ headers: { 'Content-Type': 'application/json' },
3041
+ body: JSON.stringify(payload),
3042
+ });
3043
+ }
3044
+ } catch (err) {
3045
+ addLog('Error starting simulation: ' + err.message, 'block');
3046
+ runBtn.disabled = false;
3047
+ runBtn.textContent = 'Run Simulation';
3048
+ }
3049
+ });
3050
+
3051
+ // ============================================
3052
+ // SSE — Live stream
3053
+ // ============================================
3054
+ function connectSSE() {
3055
+ const es = new EventSource('/events');
3056
+ es.onmessage = (e) => {
3057
+ try { handleEvent(JSON.parse(e.data)); } catch {}
3058
+ };
3059
+ es.onerror = () => {};
3060
+ }
3061
+
3062
+ function initChart() {
3063
+ // Chart removed — no longer needed in outcome-first view
3064
+ }
3065
+
3066
+ function addLog(msg, cls) {
3067
+ const entry = document.createElement('div');
3068
+ entry.style.cssText = 'font-size:10px;color:#666;padding:3px 0;';
3069
+ entry.textContent = msg;
3070
+ logEl.prepend(entry);
3071
+ }
3072
+
3073
+ function addTraceRound(event) {
3074
+ const el = document.createElement('div');
3075
+ el.className = 'trace-round';
3076
+
3077
+ const phaseCls = event.phase === 'baseline' ? 'baseline' : 'governed';
3078
+ const volatilityPct = (event.maxVolatility * 100).toFixed(0);
3079
+ el.innerHTML = '<div class="trace-round-header" onclick="this.nextElementSibling.dataset.collapsed = this.nextElementSibling.dataset.collapsed === \\'true\\' ? \\'false\\' : \\'true\\'">' +
3080
+ '<span class="trace-phase ' + phaseCls + '">' + event.phase + '</span>' +
3081
+ '<span class="trace-round-label">Round ' + event.round + ' / ' + event.totalRounds + '</span>' +
3082
+ '<span class="trace-round-metrics">' +
3083
+ '<span>avg: ' + event.avgImpact.toFixed(2) + '</span>' +
3084
+ '<span>vol: ' + volatilityPct + '%</span>' +
3085
+ (event.interventionCount > 0 ? '<span style="color:#fbbf24">' + event.interventionCount + ' interventions</span>' : '') +
3086
+ '</span>' +
3087
+ '</div>';
3088
+
3089
+ let bodyHtml = '';
3090
+
3091
+ // 1) Narrative events injected this round
3092
+ const roundEvents = (typeof narrativeEventsByRound !== 'undefined' && narrativeEventsByRound[event.round]) || [];
3093
+ if (roundEvents.length > 0) {
3094
+ bodyHtml += '<div class="trace-section"><div class="trace-section-label event">Event Injection</div>';
3095
+ roundEvents.forEach(function(ev) {
3096
+ bodyHtml += '<div class="trace-event-item">' + ev.headline + ' <span class="trace-event-severity ' + ev.severity + '">' + ev.severity + '</span></div>';
3097
+ });
3098
+ bodyHtml += '</div><div class="trace-arrow">↓</div>';
3099
+ }
3100
+
3101
+ // 2) Agent reactions (cap at 30 to prevent DOM bloat with thousands of agents)
3102
+ var MAX_TRACE_AGENTS = 30;
3103
+ bodyHtml += '<div class="trace-section"><div class="trace-section-label agents">Agent Reactions (' + event.reactions.length + ')</div>';
3104
+ event.reactions.slice(0, MAX_TRACE_AGENTS).forEach(function(r) {
3105
+ const impactCls = r.impact >= 0 ? 'positive' : 'negative';
3106
+ bodyHtml += '<div class="trace-agent-item">' +
3107
+ '<span class="trace-agent-name">' + r.stakeholder_id + '</span>' +
3108
+ '<span class="trace-agent-action">' + r.reaction + '</span>' +
3109
+ '<span class="trace-agent-impact ' + impactCls + '">' + r.impact.toFixed(2) + '</span>' +
3110
+ '</div>';
3111
+ });
3112
+ if (event.reactions.length > MAX_TRACE_AGENTS) {
3113
+ var traceRemaining = event.reactions.length - MAX_TRACE_AGENTS;
3114
+ 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>';
3115
+ }
3116
+ bodyHtml += '</div>';
3117
+
3118
+ // 3) Governance interventions
3119
+ const governed = event.reactions.filter(function(r) { return r.verdict && r.verdict.status !== 'ALLOW'; });
3120
+ if (governed.length > 0) {
3121
+ bodyHtml += '<div class="trace-arrow">↓</div>';
3122
+ bodyHtml += '<div class="trace-section"><div class="trace-section-label governance">Governance Interventions</div>';
3123
+ governed.forEach(function(r) {
3124
+ bodyHtml += '<div class="trace-gov-item">' +
3125
+ '<span class="verdict ' + r.verdict.status + '">' + r.verdict.status + '</span>' +
3126
+ (r.verdict.ruleId ? '<span class="trace-gov-rule">[' + r.verdict.ruleId + ']</span>' : '') +
3127
+ '<span class="trace-gov-reason">' + r.stakeholder_id + ': ' + (r.verdict.reason || '') + '</span>' +
3128
+ '</div>';
3129
+ });
3130
+ bodyHtml += '</div>';
3131
+ }
3132
+
3133
+ // 4) Emergent dynamics
3134
+ if (event.dynamics && event.dynamics.length > 0) {
3135
+ bodyHtml += '<div class="trace-arrow">↓</div>';
3136
+ bodyHtml += '<div class="trace-section"><div class="trace-section-label" style="color:#7c3aed">Emergent Dynamics</div>';
3137
+ event.dynamics.forEach(function(d) {
3138
+ bodyHtml += '<div class="trace-dynamics">' + d + '</div>';
3139
+ });
3140
+ bodyHtml += '</div>';
3141
+ }
3142
+
3143
+ const bodyEl = document.createElement('div');
3144
+ bodyEl.className = 'trace-body';
3145
+ bodyEl.dataset.collapsed = 'false';
3146
+ bodyEl.innerHTML = bodyHtml;
3147
+ el.appendChild(bodyEl);
3148
+
3149
+ logEl.prepend(el);
3150
+ }
3151
+
3152
+ function renderAgents(reactions) {
3153
+ var MAX_AGENT_ROWS = 50;
3154
+ var shown = reactions.slice(0, MAX_AGENT_ROWS);
3155
+ var html = shown.map(r => {
3156
+ const pct = Math.abs(r.impact) * 50;
3157
+ const cls = r.impact >= 0 ? 'positive' : 'negative';
3158
+ const dir = r.impact >= 0 ? 'right:50%;width:' + pct + '%' : 'left:50%;width:' + pct + '%';
3159
+ const verdictHtml = r.verdict ? ' <span class="verdict ' + r.verdict.status + '">' + r.verdict.status + '</span>' : '';
3160
+ return '<div class="agent-row">' +
3161
+ '<span class="agent-name">' + r.stakeholder_id + verdictHtml + '</span>' +
3162
+ '<div class="impact-bar-bg"><div class="center-line"></div><div class="impact-bar ' + cls + '" style="' + dir + '"></div></div>' +
3163
+ '<span class="impact-val">' + r.impact.toFixed(2) + '</span>' +
3164
+ '</div>';
3165
+ }).join('');
3166
+ if (reactions.length > MAX_AGENT_ROWS) {
3167
+ var remaining = reactions.length - MAX_AGENT_ROWS;
3168
+ var avgImpact = reactions.slice(MAX_AGENT_ROWS).reduce(function(s, r) { return s + r.impact; }, 0) / remaining;
3169
+ html += '<div class="agent-row" style="color:#555;font-size:10px;justify-content:center">+ ' + remaining + ' more agents (avg impact: ' + avgImpact.toFixed(2) + ')</div>';
3170
+ }
3171
+ agentsEl.innerHTML = html;
3172
+ }
3173
+
3174
+ // ============================================
3175
+ // PANEL RENDERING — Outcome / Behavior / Why
3176
+ // ============================================
3177
+
3178
+ function recordBehavior(reactions, round) {
3179
+ if (!reactions || !reactions.length) return;
3180
+ reactions.forEach(function(r) {
3181
+ var status = r.verdict ? r.verdict.status : 'ALLOW';
3182
+ var reason = r.verdict ? (r.verdict.reason || '') : '';
3183
+ behaviorLog.push({
3184
+ agent: r.stakeholder_id,
3185
+ action: r.reaction || 'acted',
3186
+ status: status,
3187
+ reason: reason,
3188
+ round: round || 0,
3189
+ ts: Date.now(),
3190
+ });
3191
+ });
3192
+ }
3193
+
3194
+ function updateOutcomePanel() {
3195
+ var worldThesis = currentWorld ? (currentWorld.thesis || currentWorld.description || '') : '';
3196
+ var outcome = generateOutcome(latestStability, latestVolatility, shiftTracker, worldThesis);
3197
+ outcomeStatementEl.textContent = outcome;
3198
+
3199
+ // Structured confidence card
3200
+ var conf = computeConfidence(latestStability, latestVolatility, totalInterventions, totalActions);
3201
+ confidenceGridEl.style.display = 'grid';
3202
+ document.getElementById('cc-strength').textContent = conf.strength;
3203
+ document.getElementById('cc-strength').className = 'cc-value ' + conf.strengthCls;
3204
+ document.getElementById('cc-evidence').textContent = conf.evidence;
3205
+ document.getElementById('cc-evidence').className = 'cc-value ' + conf.evidenceCls;
3206
+ document.getElementById('cc-risk').textContent = conf.risk;
3207
+ document.getElementById('cc-risk').className = 'cc-value ' + conf.riskCls;
3208
+
3209
+ // Context line
3210
+ outcomeContextEl.textContent = 'Based on ' + totalActions + ' agent action' + (totalActions !== 1 ? 's' : '') +
3211
+ (shiftTracker.total > 0 ? ' across ' + (document.getElementById('m-round').textContent || '') : '');
3212
+ }
3213
+
3214
+ function updateBehaviorPanel() {
3215
+ // Quantified behavioral shifts in human language
3216
+ var shifts = generateBehaviorShifts(shiftTracker, behaviorLog);
3217
+ if (shifts.length > 0) {
3218
+ var html = shifts.map(function(s) {
3219
+ return '<div class="behavior-shift-item">' + s + '</div>';
3220
+ }).join('');
3221
+ behaviorShiftsEl.innerHTML = html;
3222
+ }
3223
+
3224
+ // Activity timeline (narrative micro-events, expandable)
3225
+ if (behaviorLog.length > 0) {
3226
+ activityToggleEl.style.display = 'block';
3227
+ var recent = behaviorLog.slice(-30).reverse();
3228
+ var tHtml = recent.map(function(e) {
3229
+ var narrative = translateBehaviorNarrative(e.action, { status: e.status });
3230
+ return '<div class="activity-item"><span class="activity-agent">' + e.agent + '</span> ' + narrative + '</div>';
3231
+ }).join('');
3232
+ activityTimelineEl.innerHTML = tHtml;
3233
+ }
3234
+ }
3235
+
3236
+ function updateWhyPanel() {
3237
+ var causes = generateCausation(shiftTracker, behaviorLog);
3238
+ if (causes.length > 0) {
3239
+ var html = causes.map(function(c) {
3240
+ return '<div class="why-item">' + c + '</div>';
3241
+ }).join('');
3242
+ whyContentEl.innerHTML = html;
3243
+ }
3244
+ }
3245
+
3246
+ function updateAllPanels() {
3247
+ updateOutcomePanel();
3248
+ updateBehaviorPanel();
3249
+ updateWhyPanel();
3250
+ }
3251
+
3252
+ function handleEvent(event) {
3253
+ if (event.type === 'meta') {
3254
+ statusEl.className = 'status live';
3255
+ statusEl.textContent = 'LIVE';
3256
+ // Show simulation source
3257
+ const src = event.source || 'nv-sim';
3258
+ if (src !== 'nv-sim') {
3259
+ traceSourceEl.textContent = '● ' + src.toUpperCase() + ' (LIVE)';
3260
+ traceSourceEl.style.color = '#4ade80';
3261
+ engineStatusEl.textContent = 'Streaming from ' + src;
3262
+ engineStatusEl.style.color = '#4ade80';
3263
+ } else {
3264
+ traceSourceEl.textContent = '';
3265
+ engineStatusEl.textContent = '';
3266
+ }
3267
+ addLog('Simulation started: ' + event.agents.length + ' agents, ' + event.totalRounds + ' rounds' + (src !== 'nv-sim' ? ' [source: ' + src + ']' : ''));
3268
+ resetShiftTracker();
3269
+ // Store narrative events by round for trace rendering
3270
+ narrativeEventsByRound = {};
3271
+ (event.narrativeEvents || []).forEach(function(ev) {
3272
+ if (!narrativeEventsByRound[ev.round]) narrativeEventsByRound[ev.round] = [];
3273
+ narrativeEventsByRound[ev.round].push(ev);
3274
+ });
3275
+ function parseRuleTitle(id, text) {
3276
+ var parts = text.split(/\s*[—–-]{1,}\s*/);
3277
+ var title = parts.length > 1 ? parts[0].trim() : id.replace(/[-_]/g, ' ');
3278
+ var desc = parts.length > 1 ? parts.slice(1).join(' — ').trim() : text;
3279
+ title = title.replace(/\b\w/g, function(c) { return c.toUpperCase(); });
3280
+ return { title: title, desc: desc };
3281
+ }
3282
+ function generateWhy(text, type) {
3283
+ var t = text.toLowerCase();
3284
+ if (t.includes('liquidity') || t.includes('drain')) return 'Prevents system collapse from liquidity crises';
3285
+ if (t.includes('panic') || t.includes('cascade')) return 'Stops runaway feedback loops during market shocks';
3286
+ if (t.includes('leverage')) return 'Limits systemic risk from over-leveraged positions';
3287
+ if (t.includes('price') || t.includes('pricing')) return 'Stabilizes pricing mechanisms during volatility';
3288
+ if (t.includes('sentiment') || t.includes('consumer')) return 'Tracks behavioral feedback between price and confidence';
3289
+ if (t.includes('military') || t.includes('escalat')) return 'Models how escalation constrains available options';
3290
+ if (t.includes('diplomatic') || t.includes('window')) return 'Preserves negotiation pathways before they close';
3291
+ if (t.includes('grid') || t.includes('capacity')) return 'Prevents infrastructure overload from demand surges';
3292
+ if (t.includes('election') || t.includes('political')) return 'Captures how political pressure amplifies responses';
3293
+ if (t.includes('supply') || t.includes('energy') || t.includes('oil')) return 'Guards against cascading supply chain disruptions';
3294
+ if (t.includes('fraud') || t.includes('suspicious')) return 'Detects and contains anomalous behavior patterns';
3295
+ if (t.includes('withdraw') || t.includes('limit')) return 'Constrains individual actions to protect system stability';
3296
+ if (type === 'gate') return 'Blocks dangerous actions before they propagate';
3297
+ if (type === 'warning') return 'Provides early warning before thresholds are breached';
3298
+ return 'Maintains system integrity under stress conditions';
3299
+ }
3300
+ // Reset per-rule impact counters
3301
+ ruleImpactTracker = {};
3302
+ activeInvEl.innerHTML = event.invariants.map(function(inv) {
3303
+ var parsed = parseRuleTitle(inv.id, inv.description);
3304
+ var why = generateWhy(inv.description, 'invariant');
3305
+ var isUser = inv.source === 'user';
3306
+ var isFull = inv.enforcement === 'full';
3307
+ var enfLabel = isFull ? 'Fully enforced across system' : 'Advisory only';
3308
+ var enfIcon = isFull ? '&#x2713;' : '&#x26A0;';
3309
+ var sourceTag = isUser ? '<span class="rule-source-tag">USER RULE</span>' : '';
3310
+ ruleImpactTracker[inv.id] = { blocks: 0, label: parsed.title };
3311
+ return '<div class="rule-card type-invariant' + (isUser ? ' user-rule' : '') + '" data-rule-id="' + inv.id + '">' +
3312
+ '<div class="rule-header"><span class="rule-icon">&#x1F7E2;</span><span class="rule-title">' + parsed.title + '</span>' + sourceTag + '</div>' +
3313
+ '<div class="rule-desc">' + parsed.desc + '</div>' +
3314
+ '<div class="rule-meta">Invariant &bull; ' + enfIcon + ' ' + enfLabel + '</div>' +
3315
+ '<div class="rule-why">' + why + '</div>' +
3316
+ '<div class="rule-impact" data-impact-id="' + inv.id + '"></div>' +
3317
+ '</div>';
3318
+ }).join('') + event.gates.map(function(g) {
3319
+ var isCritical = g.severity === 'critical';
3320
+ var typeClass = isCritical ? 'type-gate' : 'type-warning';
3321
+ var icon = isCritical ? '&#x1F534;' : '&#x1F7E1;';
3322
+ var typeLabel = isCritical ? 'Gate' : 'Warning';
3323
+ var effect = isCritical ? 'Blocks actions' : 'Signals risk';
3324
+ var why = generateWhy(g.label + ' ' + (g.condition || ''), isCritical ? 'gate' : 'warning');
3325
+ ruleImpactTracker[g.id] = { blocks: 0, label: g.label };
3326
+ return '<div class="rule-card ' + typeClass + '" data-rule-id="' + g.id + '">' +
3327
+ '<div class="rule-header"><span class="rule-icon">' + icon + '</span><span class="rule-title">' + g.label + '</span></div>' +
3328
+ '<div class="rule-desc">' + (g.condition || g.label) + '</div>' +
3329
+ '<div class="rule-meta">' + typeLabel + ' &bull; ' + effect + '</div>' +
3330
+ '<div class="rule-why">' + why + '</div>' +
3331
+ '<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
3332
+ '</div>';
3333
+ }).join('');
3334
+ }
3335
+
3336
+ if (event.type === 'round') {
3337
+ const isBridge = event.reactions && event.reactions[0] && event.reactions[0].trigger === 'bridge';
3338
+
3339
+ // Record behavior data (no governance display)
3340
+ recordBehavior(event.reactions, event.round);
3341
+ renderAgents(event.reactions); // hidden, kept for audit
3342
+
3343
+ // Track totals
3344
+ totalActions += event.reactions ? event.reactions.length : 0;
3345
+ totalInterventions += event.interventionCount;
3346
+ latestVolatility = event.maxVolatility || 0;
3347
+
3348
+ // Update hidden data elements for audit
3349
+ if (isBridge) {
3350
+ document.getElementById('m-round').textContent = event.round + ' evals';
3351
+ } else {
3352
+ document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
3353
+ }
3354
+ document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
3355
+ document.getElementById('m-interventions').textContent = totalInterventions;
3356
+
3357
+ // Track system shifts (data collection, not displayed)
3358
+ trackShift(event);
3359
+
3360
+ // Update all visible panels
3361
+ updateAllPanels();
3362
+
3363
+ // Add trace entry for audit
3364
+ addTraceRound(event);
3365
+ }
3366
+
3367
+ // Bridge metrics — updates stability from external /api/evaluate calls
3368
+ if (event.type === 'bridge_metrics') {
3369
+ latestStability = event.stability;
3370
+ document.getElementById('m-stability').textContent = (event.stability * 100).toFixed(0) + '%';
3371
+ updateAllPanels();
3372
+ }
3373
+
3374
+ if (event.type === 'complete') {
3375
+ statusEl.className = 'status complete';
3376
+ statusEl.textContent = 'COMPLETE';
3377
+ const r = event.result;
3378
+ if (r.governed) {
3379
+ latestStability = r.governed.metrics.stabilityScore;
3380
+ latestVolatility = r.governed.metrics.maxVolatility || latestVolatility;
3381
+ document.getElementById('m-stability').textContent = (r.governed.metrics.stabilityScore * 100).toFixed(0) + '%';
3382
+ addLog('Simulation complete');
3383
+ renderSystemShift(r);
3384
+ renderRuleImpacts(r);
3385
+ renderEnforcementClassification(r.enforcementClassification || []);
3386
+ updateAllPanels();
3387
+ lastSimResult = {
3388
+ stability: r.governed.metrics.stabilityScore,
3389
+ volatility: r.governed.metrics.maxVolatility,
3390
+ collapseProbability: r.governed.metrics.collapseProbability,
3391
+ governanceEffectiveness: r.comparison.governanceEffectiveness,
3392
+ };
3393
+ }
3394
+ runBtn.disabled = false;
3395
+ runBtn.textContent = 'Run Simulation';
3396
+ }
3397
+ }
3398
+
3399
+ // ============================================
3400
+ // PER-RULE IMPACT RENDERING
3401
+ // ============================================
3402
+ function renderRuleImpacts(result) {
3403
+ var totalBlocks = shiftTracker.blocks;
3404
+ var cascadeAvoided = result && result.governed && result.baseline &&
3405
+ result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
3406
+
3407
+ Object.keys(ruleImpactTracker).forEach(function(ruleId) {
3408
+ var tracker = ruleImpactTracker[ruleId];
3409
+ var el = document.querySelector('[data-impact-id="' + ruleId + '"]');
3410
+ if (!el) return;
3411
+
3412
+ var html = '';
3413
+ if (tracker.blocks > 0) {
3414
+ html += '<span class="impact-stat">' + tracker.blocks + ' action' + (tracker.blocks > 1 ? 's' : '') + ' blocked</span>';
3415
+ if (cascadeAvoided) {
3416
+ html += ' <span class="impact-label">&rarr; cascade avoided</span>';
3417
+ } else {
3418
+ html += ' <span class="impact-label">&rarr; behavior modified</span>';
3419
+ }
3420
+ } else {
3421
+ // Rule was present but didn't fire — still useful info
3422
+ html += '<span class="impact-label">No violations detected &mdash; agents complied</span>';
3423
+ }
3424
+ el.innerHTML = html;
3425
+ el.classList.add('visible');
3426
+ });
3427
+ }
3428
+
3429
+ // ============================================
3430
+ // ENFORCEMENT CLASSIFICATION — Full vs Advisory
3431
+ // ============================================
3432
+ function renderEnforcementClassification(entries) {
3433
+ if (!entries || entries.length === 0) return;
3434
+ entries.forEach(function(entry) {
3435
+ var card = document.querySelector('[data-rule-id="' + entry.id + '"]');
3436
+ if (!card) return;
3437
+ var metaEl = card.querySelector('.rule-meta');
3438
+ if (!metaEl) return;
3439
+ if (entry.level === 'full' && entry.fired) {
3440
+ metaEl.innerHTML = 'Invariant &bull; &#x2713; Fully enforced &bull; <span style="color:#22c55e;font-weight:600">FIRED</span>';
3441
+ card.style.borderLeftColor = '#22c55e';
3442
+ } else if (entry.level === 'full' && !entry.fired) {
3443
+ metaEl.innerHTML = 'Invariant &bull; &#x2713; Fully enforced &bull; <span style="color:var(--text-muted)">standby</span>';
3444
+ } else if (entry.level === 'advisory') {
3445
+ metaEl.innerHTML = 'Invariant &bull; &#x26A0; Advisory only &bull; <span style="color:var(--text-muted)">monitored</span>';
3446
+ card.style.borderLeftColor = '#6b7280';
3447
+ card.style.opacity = '0.75';
3448
+ }
3449
+ });
3450
+ }
3451
+
3452
+ // ============================================
3453
+ // SYSTEM SHIFT CARD — The Demo Moment
3454
+ // ============================================
3455
+ const ssCard = document.getElementById('system-shift');
3456
+ const ssRuleEl = document.getElementById('ss-rule');
3457
+ const ssAdaptRateEl = document.getElementById('ss-adapt-rate');
3458
+ const ssAdaptDescEl = document.getElementById('ss-adapt-desc');
3459
+ const ssShiftsEl = document.getElementById('ss-shifts');
3460
+ const ssPatternsEl = document.getElementById('ss-patterns');
3461
+ const ssImpactsEl = document.getElementById('ss-impacts');
3462
+ const ssNarrativeEl = document.getElementById('ss-narrative');
3463
+
3464
+ // Raw detail toggle
3465
+ var ssRawToggle = document.getElementById('ss-raw-toggle');
3466
+ if (ssRawToggle) {
3467
+ ssRawToggle.addEventListener('click', function() {
3468
+ this.classList.toggle('open');
3469
+ document.getElementById('ss-raw-detail').classList.toggle('open');
3470
+ });
3471
+ }
3472
+
3473
+ let shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
3474
+
3475
+ function resetShiftTracker() {
3476
+ shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
3477
+ if (ssCard) ssCard.classList.remove('visible');
3478
+ var rawToggle = document.getElementById('ss-raw-toggle');
3479
+ var rawDetail = document.getElementById('ss-raw-detail');
3480
+ if (rawToggle) rawToggle.classList.remove('open');
3481
+ if (rawDetail) rawDetail.classList.remove('open');
3482
+ // Reset behavior tracking
3483
+ behaviorLog = [];
3484
+ totalActions = 0;
3485
+ totalInterventions = 0;
3486
+ latestStability = 0;
3487
+ latestVolatility = 0;
3488
+ // Reset visible panels
3489
+ if (outcomeStatementEl) outcomeStatementEl.innerHTML = '<span class="outcome-empty">Run a simulation or connect an agent to see outcomes</span>';
3490
+ if (confidenceGridEl) confidenceGridEl.style.display = 'none';
3491
+ if (outcomeContextEl) outcomeContextEl.textContent = '';
3492
+ if (behaviorShiftsEl) behaviorShiftsEl.innerHTML = '<div class="behavior-empty">Waiting for agent actions</div>';
3493
+ if (activityToggleEl) activityToggleEl.style.display = 'none';
3494
+ if (activityTimelineEl) { activityTimelineEl.innerHTML = ''; activityTimelineEl.classList.remove('open'); }
3495
+ if (whyContentEl) whyContentEl.innerHTML = '<div class="why-empty">Causation analysis appears after agent actions are evaluated</div>';
3496
+ }
3497
+
3498
+ function trackShift(event) {
3499
+ if (!event.reactions) return;
3500
+ const governed = event.reactions.filter(function(r) { return r.verdict && r.verdict.status !== 'ALLOW'; });
3501
+ shiftTracker.blocks += governed.length;
3502
+ shiftTracker.total += event.reactions.length;
3503
+ governed.forEach(function(r) {
3504
+ const key = r.verdict.status + ': ' + (r.reaction || 'adapted');
3505
+ shiftTracker.shifts[key] = (shiftTracker.shifts[key] || 0) + 1;
3506
+ // Track per-rule impacts for card display
3507
+ var ruleId = r.verdict.ruleId || '';
3508
+ if (ruleId && ruleImpactTracker[ruleId]) {
3509
+ ruleImpactTracker[ruleId].blocks++;
3510
+ } else {
3511
+ // Try to match by pattern — governance may report with NV- prefix
3512
+ Object.keys(ruleImpactTracker).forEach(function(k) {
3513
+ if (ruleId.includes(k) || (r.verdict.reason && r.verdict.reason.toLowerCase().includes(ruleImpactTracker[k].label.toLowerCase()))) {
3514
+ ruleImpactTracker[k].blocks++;
3515
+ }
3516
+ });
3517
+ }
3518
+ // Store raw governed reactions for detail view
3519
+ shiftTracker.rawGoverned.push({
3520
+ agent: r.stakeholder_id,
3521
+ action: r.reaction,
3522
+ status: r.verdict.status,
3523
+ reason: r.verdict.reason || '',
3524
+ round: event.round,
3525
+ });
3526
+ });
3527
+ if (event.phase === 'baseline') shiftTracker.baselineVol = Math.max(shiftTracker.baselineVol, event.maxVolatility || 0);
3528
+ if (event.phase === 'governed') shiftTracker.governedVol = Math.max(shiftTracker.governedVol, event.maxVolatility || 0);
3529
+ if (event.dynamics) {
3530
+ event.dynamics.forEach(function(d) {
3531
+ if (shiftTracker.patterns.indexOf(d) === -1) shiftTracker.patterns.push(d);
3532
+ });
3533
+ }
3534
+ }
3535
+
3536
+ function renderSystemShift(result) {
3537
+ if (shiftTracker.blocks === 0) return;
3538
+ if (!ssCard) return; // System shift card removed from visible UI
3539
+
3540
+ var adaptRate = shiftTracker.total > 0 ? Math.round((shiftTracker.blocks / shiftTracker.total) * 100) : 0;
3541
+
3542
+ // Determine rule label from world
3543
+ var ruleName = 'Governance Rules Active';
3544
+ if (currentWorld && currentWorld.invariants) {
3545
+ var names = currentWorld.invariants.map(function(inv) { return inv.description || inv.id; });
3546
+ ruleName = names.slice(0, 2).join(' + ');
3547
+ }
3548
+
3549
+ ssRuleEl.textContent = ruleName;
3550
+ ssAdaptRateEl.textContent = adaptRate + '%';
3551
+ ssAdaptDescEl.textContent = 'adaptation across ' + shiftTracker.total.toLocaleString() + ' agents';
3552
+
3553
+ // Scale line — proves this is real
3554
+ var scaleEl = document.getElementById('ss-scale');
3555
+ scaleEl.innerHTML = '<strong>' + shiftTracker.blocks.toLocaleString() + '</strong> actions reshaped out of <strong>' + shiftTracker.total.toLocaleString() + '</strong> total';
3556
+
3557
+ // Shifts — show top 5 by count, collapse the rest
3558
+ var shiftHtml = '';
3559
+ var shiftKeys = Object.keys(shiftTracker.shifts).sort(function(a, b) {
3560
+ return shiftTracker.shifts[b] - shiftTracker.shifts[a];
3561
+ });
3562
+ var MAX_SHIFTS = 5;
3563
+ shiftKeys.slice(0, MAX_SHIFTS).forEach(function(k) {
3564
+ shiftHtml += '<div class="ss-shift-item"><span class="ss-shift-arrow">→</span> ' + k + ' (' + shiftTracker.shifts[k] + ' agents)</div>';
3565
+ });
3566
+ if (shiftKeys.length > MAX_SHIFTS) {
3567
+ var remaining = shiftKeys.slice(MAX_SHIFTS).reduce(function(sum, k) { return sum + shiftTracker.shifts[k]; }, 0);
3568
+ shiftHtml += '<div class="ss-shift-item" style="color:#555">+ ' + (shiftKeys.length - MAX_SHIFTS) + ' more patterns (' + remaining + ' agents)</div>';
3569
+ }
3570
+ ssShiftsEl.innerHTML = shiftHtml;
3571
+
3572
+ // Patterns
3573
+ var patternHtml = '';
3574
+ if (shiftTracker.patterns.length > 0) {
3575
+ shiftTracker.patterns.forEach(function(p) {
3576
+ patternHtml += '<span class="ss-pattern-tag">' + p + '</span>';
3577
+ });
3578
+ } else {
3579
+ // Infer patterns from data
3580
+ if (shiftTracker.blocks >= 3) patternHtml += '<span class="ss-pattern-tag">Coordinated Holding</span>';
3581
+ if (shiftTracker.blocks >= 2) patternHtml += '<span class="ss-pattern-tag">Panic Suppression</span>';
3582
+ if (shiftTracker.governedVol < shiftTracker.baselineVol) patternHtml += '<span class="ss-pattern-tag">Stability Shift</span>';
3583
+ }
3584
+ ssPatternsEl.innerHTML = patternHtml || '<span style="font-size:11px;color:#555">No emergent patterns detected</span>';
3585
+
3586
+ // Impact
3587
+ var impactHtml = '';
3588
+ var volDelta = shiftTracker.governedVol - shiftTracker.baselineVol;
3589
+ impactHtml += '<div class="ss-impact-row"><span>Volatility</span><span class="ss-impact-delta' + (volDelta > 0 ? ' negative' : '') + '">' +
3590
+ (shiftTracker.baselineVol * 100).toFixed(0) + '% → ' + (shiftTracker.governedVol * 100).toFixed(0) + '%</span></div>';
3591
+
3592
+ if (result && result.governed && result.baseline) {
3593
+ var stabDelta = result.governed.metrics.stabilityScore - result.baseline.metrics.stabilityScore;
3594
+ impactHtml += '<div class="ss-impact-row"><span>Stability</span><span class="ss-impact-delta">' +
3595
+ (result.baseline.metrics.stabilityScore * 100).toFixed(0) + '% → ' + (result.governed.metrics.stabilityScore * 100).toFixed(0) + '%</span></div>';
3596
+
3597
+ var cascadeAvoided = result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
3598
+ if (cascadeAvoided) {
3599
+ impactHtml += '<div class="ss-impact-row"><span>Cascade</span><span class="ss-impact-delta">Avoided</span></div>';
3600
+ }
3601
+ }
3602
+ ssImpactsEl.innerHTML = impactHtml;
3603
+
3604
+ // Narrative — tight cause → shift → outcome
3605
+ var topShift = shiftKeys.length > 0 ? shiftKeys[0].split(': ')[1] || 'adapted' : 'adapted';
3606
+ var narrative = '';
3607
+
3608
+ // What was the rule?
3609
+ narrative += ruleName + '. ';
3610
+
3611
+ // What shifted?
3612
+ narrative += shiftTracker.blocks + ' of ' + shiftTracker.total + ' agents reorganized';
3613
+ if (topShift !== 'adapted') narrative += ' — most shifted to ' + topShift;
3614
+ narrative += '. ';
3615
+
3616
+ // What emerged?
3617
+ if (shiftTracker.patterns.length > 0) {
3618
+ narrative += 'Pattern: ' + shiftTracker.patterns.slice(0, 2).join(', ') + '. ';
3619
+ }
3620
+
3621
+ // What was the outcome?
3622
+ if (volDelta < 0) narrative += 'Volatility dropped ' + Math.abs(volDelta * 100).toFixed(0) + '%.';
3623
+ else if (volDelta === 0) narrative += 'System held steady.';
3624
+ else narrative += 'Volatility increased ' + (volDelta * 100).toFixed(0) + '% — rules need tuning.';
3625
+
3626
+ ssNarrativeEl.textContent = narrative;
3627
+
3628
+ // Raw detail — virtualized list of governed actions (collapsed by default)
3629
+ var rawListEl = document.getElementById('ss-raw-list');
3630
+ var MAX_RAW = 100;
3631
+ var rawItems = shiftTracker.rawGoverned.slice(0, MAX_RAW);
3632
+ var rawHtml = rawItems.map(function(r) {
3633
+ return '<div class="ss-raw-item">' +
3634
+ '<span class="raw-agent">' + r.agent + '</span>' +
3635
+ '<span class="raw-action">R' + r.round + ': ' + r.action + '</span>' +
3636
+ '<span class="raw-verdict ' + r.status + '">' + r.status + '</span>' +
3637
+ '</div>';
3638
+ }).join('');
3639
+ if (shiftTracker.rawGoverned.length > MAX_RAW) {
3640
+ rawHtml += '<div style="font-size:10px;color:#444;padding:4px 0">+ ' + (shiftTracker.rawGoverned.length - MAX_RAW) + ' more governed actions</div>';
3641
+ }
3642
+ rawListEl.innerHTML = rawHtml;
3643
+
3644
+ ssCard.classList.add('visible');
3645
+ }
3646
+
3647
+ // ============================================
3648
+ // WORLD VARIANTS — Save / Load / Delete
3649
+ // ============================================
3650
+ const variantListEl = document.getElementById('variant-list');
3651
+ const saveBtn = document.getElementById('save-btn');
3652
+ const saveForm = document.getElementById('save-form');
3653
+ const confirmSaveBtn = document.getElementById('confirm-save-btn');
3654
+ const cancelSaveBtn = document.getElementById('cancel-save-btn');
3655
+ const variantNameInput = document.getElementById('variant-name');
3656
+ const variantDescInput = document.getElementById('variant-desc');
3657
+ let lastSimResult = null;
3658
+
3659
+ saveBtn.addEventListener('click', () => {
3660
+ saveForm.style.display = saveForm.style.display === 'none' ? '' : 'none';
3661
+ variantNameInput.focus();
3662
+ });
3663
+
3664
+ cancelSaveBtn.addEventListener('click', () => {
3665
+ saveForm.style.display = 'none';
3666
+ });
3667
+
3668
+ confirmSaveBtn.addEventListener('click', async () => {
3669
+ const name = variantNameInput.value.trim();
3670
+ if (!name || !currentWorld) return;
3671
+
3672
+ // Gather current state overrides
3673
+ const stateOverrides = {};
3674
+ if (currentWorld.stateVariables) {
3675
+ currentWorld.stateVariables.forEach(sv => {
3676
+ const el = document.getElementById('sv-' + sv.id);
3677
+ if (!el) return;
3678
+ if (sv.type === 'number') stateOverrides[sv.id] = parseFloat(el.value);
3679
+ else if (sv.type === 'boolean') stateOverrides[sv.id] = el.classList.contains('on');
3680
+ else stateOverrides[sv.id] = el.value;
3681
+ });
3682
+ }
3683
+
3684
+ const payload = {
3685
+ name,
3686
+ description: variantDescInput.value.trim(),
3687
+ baseWorld: currentWorld.id,
3688
+ stateOverrides,
3689
+ events: injectedEvents.slice(),
3690
+ rounds: parseInt(roundsSlider.value),
3691
+ lastResult: lastSimResult,
3692
+ };
3693
+
3694
+ try {
3695
+ const resp = await fetch('/api/save-variant', {
3696
+ method: 'POST',
3697
+ headers: { 'Content-Type': 'application/json' },
3698
+ body: JSON.stringify(payload),
3699
+ });
3700
+ const data = await resp.json();
3701
+ if (data.status === 'saved') {
3702
+ addLog('Variant saved: ' + data.variant.name + ' (' + data.variant.id + ')', '');
3703
+ saveForm.style.display = 'none';
3704
+ variantNameInput.value = '';
3705
+ variantDescInput.value = '';
3706
+ await loadVariants();
3707
+ }
3708
+ } catch (err) {
3709
+ addLog('Error saving variant: ' + err.message, 'block');
3710
+ }
3711
+ });
3712
+
3713
+ async function loadVariants() {
3714
+ try {
3715
+ const resp = await fetch('/api/variants');
3716
+ const data = await resp.json();
3717
+ renderVariants(data.variants || []);
3718
+ } catch {}
3719
+ }
3720
+
3721
+ function renderVariants(variants) {
3722
+ if (variants.length === 0) {
3723
+ variantListEl.innerHTML = '<div style="font-size:11px;color:#333">No saved variants yet</div>';
3724
+ return;
3725
+ }
3726
+ variantListEl.innerHTML = variants.map(v => {
3727
+ const resultHtml = v.lastResult
3728
+ ? '<span class="vresult">Stability: ' + (v.lastResult.stability * 100).toFixed(0) + '% | Effectiveness: ' + (v.lastResult.governanceEffectiveness * 100).toFixed(0) + '%</span>'
3729
+ : '<span style="color:#555">Not yet run</span>';
3730
+ return '<div class="variant-card" data-vid="' + v.id + '">' +
3731
+ '<div class="vname">' + v.name + '</div>' +
3732
+ (v.description ? '<div class="vdesc">' + v.description + '</div>' : '') +
3733
+ '<span class="vbase">' + v.baseWorld + '</span>' +
3734
+ '<div class="vmeta">' + resultHtml + ' | ' + v.events.length + ' events | ' + v.rounds + ' rounds</div>' +
3735
+ '<span class="vdelete" data-vid="' + v.id + '">delete</span>' +
3736
+ '</div>';
3737
+ }).join('');
3738
+
3739
+ // Bind load handlers
3740
+ variantListEl.querySelectorAll('.variant-card').forEach(card => {
3741
+ card.addEventListener('click', (e) => {
3742
+ if (e.target.classList.contains('vdelete')) return;
3743
+ const vid = card.dataset.vid;
3744
+ const v = variants.find(x => x.id === vid);
3745
+ if (v) loadVariant(v);
3746
+ });
3747
+ });
3748
+
3749
+ // Bind delete handlers
3750
+ variantListEl.querySelectorAll('.vdelete').forEach(el => {
3751
+ el.addEventListener('click', async (e) => {
3752
+ e.stopPropagation();
3753
+ const vid = el.dataset.vid;
3754
+ if (!confirm('Delete variant "' + vid + '"?')) return;
3755
+ try {
3756
+ await fetch('/api/delete-variant/' + vid, { method: 'DELETE' });
3757
+ addLog('Variant deleted: ' + vid, '');
3758
+ await loadVariants();
3759
+ } catch {}
3760
+ });
3761
+ });
3762
+ }
3763
+
3764
+ function loadVariant(variant) {
3765
+ // Set world
3766
+ selectWorld(variant.baseWorld);
3767
+
3768
+ // Apply state overrides
3769
+ if (variant.stateOverrides && currentWorld && currentWorld.stateVariables) {
3770
+ currentWorld.stateVariables.forEach(sv => {
3771
+ if (sv.id in variant.stateOverrides) {
3772
+ const el = document.getElementById('sv-' + sv.id);
3773
+ if (!el) return;
3774
+ if (sv.type === 'boolean') {
3775
+ const val = variant.stateOverrides[sv.id];
3776
+ if (val && !el.classList.contains('on')) el.classList.add('on');
3777
+ if (!val && el.classList.contains('on')) el.classList.remove('on');
3778
+ } else {
3779
+ el.value = variant.stateOverrides[sv.id];
3780
+ // Update value display for sliders
3781
+ const valEl = document.getElementById('sv-val-' + sv.id);
3782
+ if (valEl) valEl.textContent = variant.stateOverrides[sv.id];
3783
+ }
3784
+ }
3785
+ });
3786
+ }
3787
+
3788
+ // Set events
3789
+ injectedEvents = variant.events.slice();
3790
+ renderInjectedEvents();
3791
+
3792
+ // Set rounds
3793
+ roundsSlider.value = Math.min(variant.rounds, 12);
3794
+ roundsVal.textContent = Math.min(variant.rounds, 12);
3795
+
3796
+ addLog('Loaded variant: ' + variant.name, '');
3797
+ }
3798
+
3799
+ // ============================================
3800
+ // CAPTURE LAST RESULT FOR VARIANT SAVING
3801
+ // ============================================
3802
+ // (override handleEvent to capture results)
3803
+ const _origHandleEvent = handleEvent;
3804
+ handleEvent = function(event) {
3805
+ _origHandleEvent(event);
3806
+ if (event.type === 'complete' && event.result && event.result.governed) {
3807
+ lastSimResult = {
3808
+ stability: event.result.governed.metrics.stabilityScore,
3809
+ volatility: event.result.governed.metrics.maxVolatility,
3810
+ collapseProbability: event.result.governed.metrics.collapseProbability,
3811
+ governanceEffectiveness: event.result.comparison.governanceEffectiveness,
3812
+ };
3813
+ }
3814
+ };
3815
+
3816
+ // ============================================
3817
+ // THEME TOGGLE — Light / Dark mode
3818
+ // ============================================
3819
+ const themeToggleBtn = document.getElementById('theme-toggle');
3820
+ function applyTheme(theme) {
3821
+ if (theme === 'light') {
3822
+ document.body.classList.add('light');
3823
+ themeToggleBtn.textContent = 'Dark Mode';
3824
+ } else {
3825
+ document.body.classList.remove('light');
3826
+ themeToggleBtn.textContent = 'Light Mode';
3827
+ }
3828
+ localStorage.setItem('nv-theme', theme);
3829
+ }
3830
+ themeToggleBtn.addEventListener('click', () => {
3831
+ const current = document.body.classList.contains('light') ? 'light' : 'dark';
3832
+ applyTheme(current === 'light' ? 'dark' : 'light');
3833
+ });
3834
+ // Restore saved theme
3835
+ const savedTheme = localStorage.getItem('nv-theme');
3836
+ if (savedTheme) applyTheme(savedTheme);
3837
+
3838
+ // ============================================
3839
+ // WORLD SOURCE SWITCHING
3840
+ // ============================================
3841
+ let currentWorldSource = 'preset';
3842
+ const worldSourceTabs = document.querySelectorAll('.ws-tab');
3843
+ const sourcePresetPanel = document.getElementById('source-preset');
3844
+ const sourceCustomPanel = document.getElementById('source-custom');
3845
+ const sourceUploadPanel = document.getElementById('source-upload');
3846
+
3847
+ worldSourceTabs.forEach(tab => {
3848
+ tab.addEventListener('click', () => {
3849
+ const source = tab.dataset.source;
3850
+ if (source === currentWorldSource) return;
3851
+
3852
+ currentWorldSource = source;
3853
+
3854
+ // Update tab visuals
3855
+ worldSourceTabs.forEach(t => t.classList.remove('active'));
3856
+ tab.classList.add('active');
3857
+ tab.querySelector('input').checked = true;
3858
+
3859
+ // Show/hide panels
3860
+ sourcePresetPanel.style.display = source === 'preset' ? '' : 'none';
3861
+ sourceCustomPanel.style.display = source === 'custom' ? '' : 'none';
3862
+ sourceUploadPanel.style.display = source === 'upload' ? '' : 'none';
3863
+ });
3864
+ });
3865
+
3866
+ // Populate base world selector in custom rules panel
3867
+ function populateBaseWorldSelect() {
3868
+ const select = document.getElementById('custom-base-world');
3869
+ if (!select) return;
3870
+ worlds.forEach(w => {
3871
+ const opt = document.createElement('option');
3872
+ opt.value = w.id;
3873
+ opt.textContent = w.title;
3874
+ select.appendChild(opt);
3875
+ });
3876
+ }
3877
+
3878
+ // ============================================
3879
+ // WORLD ACTION BAR
3880
+ // ============================================
3881
+
3882
+ // + New World
3883
+ document.getElementById('new-world-btn').addEventListener('click', () => {
3884
+ // Switch to custom rules mode
3885
+ currentWorldSource = 'custom';
3886
+ worldSourceTabs.forEach(t => {
3887
+ t.classList.toggle('active', t.dataset.source === 'custom');
3888
+ t.querySelector('input').checked = t.dataset.source === 'custom';
3889
+ });
3890
+ sourcePresetPanel.style.display = 'none';
3891
+ sourceCustomPanel.style.display = '';
3892
+ sourceUploadPanel.style.display = 'none';
3893
+
3894
+ // Clear everything
3895
+ document.getElementById('custom-world-name').value = '';
3896
+ document.getElementById('custom-world-thesis').value = '';
3897
+ document.getElementById('rule-input').value = '';
3898
+ document.getElementById('parsed-rules').innerHTML = '';
3899
+ document.getElementById('rule-status').textContent = '';
3900
+ document.getElementById('rule-status').className = 'rule-status';
3901
+ document.getElementById('custom-base-world').value = '';
3902
+
3903
+ // Clear active rules server-side
3904
+ fetch('/api/clear-rules', { method: 'POST' });
3905
+
3906
+ // Reset right panel
3907
+ document.getElementById('active-invariants').innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No rules loaded. Define your world.</div>';
3908
+ });
3909
+
3910
+ // Clear Rules
3911
+ document.getElementById('clear-rules-btn').addEventListener('click', async () => {
3912
+ await fetch('/api/clear-rules', { method: 'POST' });
3913
+
3914
+ // Clear rule editor UI
3915
+ const ruleInput = document.getElementById('rule-input');
3916
+ if (ruleInput) ruleInput.value = '';
3917
+ const parsed = document.getElementById('parsed-rules');
3918
+ if (parsed) parsed.innerHTML = '';
3919
+ const status = document.getElementById('rule-status');
3920
+ if (status) { status.textContent = 'Rules cleared.'; status.className = 'rule-status success'; }
3921
+
3922
+ // Clear upload state
3923
+ const uploadStatus = document.getElementById('upload-status');
3924
+ if (uploadStatus) { uploadStatus.textContent = 'Rules cleared.'; uploadStatus.className = 'rule-status success'; }
3925
+ const loadedInfo = document.getElementById('loaded-world-info');
3926
+ if (loadedInfo) loadedInfo.style.display = 'none';
3927
+ });
3928
+
3929
+ // Load World File (switch to upload tab)
3930
+ document.getElementById('load-file-btn').addEventListener('click', () => {
3931
+ currentWorldSource = 'upload';
3932
+ worldSourceTabs.forEach(t => {
3933
+ t.classList.toggle('active', t.dataset.source === 'upload');
3934
+ t.querySelector('input').checked = t.dataset.source === 'upload';
3935
+ });
3936
+ sourcePresetPanel.style.display = 'none';
3937
+ sourceCustomPanel.style.display = 'none';
3938
+ sourceUploadPanel.style.display = '';
3939
+ });
3940
+
3941
+ // Save as World File (export)
3942
+ document.getElementById('export-world-btn').addEventListener('click', async () => {
3943
+ try {
3944
+ const resp = await fetch('/api/export-world');
3945
+ const data = await resp.json();
3946
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
3947
+ const url = URL.createObjectURL(blob);
3948
+ const a = document.createElement('a');
3949
+ a.href = url;
3950
+ a.download = (currentWorld ? currentWorld.id : 'custom') + '-world.json';
3951
+ document.body.appendChild(a);
3952
+ a.click();
3953
+ document.body.removeChild(a);
3954
+ URL.revokeObjectURL(url);
3955
+ } catch (err) {
3956
+ alert('Export failed: ' + err.message);
3957
+ }
3958
+ });
3959
+
3960
+ // ============================================
3961
+ // WORLD FILE UPLOAD / PASTE
3962
+ // ============================================
3963
+ const uploadZone = document.getElementById('upload-zone');
3964
+ const uploadFileInput = document.getElementById('upload-file-input');
3965
+ const uploadBrowseBtn = document.getElementById('upload-browse-btn');
3966
+ const worldJsonInput = document.getElementById('world-json-input');
3967
+ const loadWorldBtn = document.getElementById('load-world-btn');
3968
+ const uploadStatusEl = document.getElementById('upload-status');
3969
+
3970
+ // Drag and drop
3971
+ uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
3972
+ uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
3973
+ uploadZone.addEventListener('drop', (e) => {
3974
+ e.preventDefault();
3975
+ uploadZone.classList.remove('dragover');
3976
+ const file = e.dataTransfer.files[0];
3977
+ if (file) readWorldFile(file);
3978
+ });
3979
+
3980
+ // Browse button
3981
+ uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadFileInput.click(); });
3982
+ uploadFileInput.addEventListener('change', () => {
3983
+ if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]);
3984
+ });
3985
+
3986
+ // Click zone to browse
3987
+ uploadZone.addEventListener('click', () => { uploadFileInput.click(); });
3988
+
3989
+ function readWorldFile(file) {
3990
+ const reader = new FileReader();
3991
+ reader.onload = (e) => {
3992
+ worldJsonInput.value = e.target.result;
3993
+ uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".';
3994
+ uploadStatusEl.className = 'rule-status success';
3995
+ };
3996
+ reader.readAsText(file);
3997
+ }
3998
+
3999
+ // Load into Runtime
4000
+ loadWorldBtn.addEventListener('click', async () => {
4001
+ const jsonText = worldJsonInput.value.trim();
4002
+ if (!jsonText) {
4003
+ uploadStatusEl.textContent = 'Paste or upload a world file first.';
4004
+ uploadStatusEl.className = 'rule-status error';
4005
+ return;
4006
+ }
4007
+
4008
+ let worldData;
4009
+ try {
4010
+ worldData = JSON.parse(jsonText);
4011
+ } catch (err) {
4012
+ uploadStatusEl.textContent = 'Invalid JSON: ' + err.message;
4013
+ uploadStatusEl.className = 'rule-status error';
4014
+ return;
4015
+ }
4016
+
4017
+ // Normalize: if the JSON is { world: {...} } or just {...}
4018
+ const worldPayload = worldData.world || worldData;
4019
+
4020
+ loadWorldBtn.textContent = 'Loading...';
4021
+ loadWorldBtn.disabled = true;
4022
+
4023
+ try {
4024
+ const resp = await fetch('/api/load-world-file', {
4025
+ method: 'POST',
4026
+ headers: { 'Content-Type': 'application/json' },
4027
+ body: JSON.stringify({ world: worldPayload }),
4028
+ });
4029
+ const result = await resp.json();
4030
+
4031
+ if (result.error) {
4032
+ uploadStatusEl.textContent = result.error;
4033
+ uploadStatusEl.className = 'rule-status error';
4034
+ } else {
4035
+ uploadStatusEl.textContent = result.message;
4036
+ uploadStatusEl.className = 'rule-status success';
4037
+
4038
+ // Show loaded world info
4039
+ const infoEl = document.getElementById('loaded-world-info');
4040
+ infoEl.style.display = '';
4041
+ document.getElementById('lw-name').textContent = result.world.title;
4042
+ document.getElementById('lw-thesis').textContent = '"' + result.world.thesis + '"';
4043
+ document.getElementById('lw-stats').textContent =
4044
+ result.world.invariants.length + ' invariants, ' +
4045
+ result.world.gates.length + ' gates, ' +
4046
+ result.rulesApplied + ' rules';
4047
+
4048
+ // Update active invariants in right panel
4049
+ const invHtml = result.world.invariants.map(inv =>
4050
+ '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
4051
+ ).join('') + result.world.gates.map(g =>
4052
+ '<div class="inv-item" style="color:' + (g.severity === 'critical' ? 'var(--red)' : 'var(--yellow)') + '">[' + g.id + '] ' + g.label + '</div>'
4053
+ ).join('');
4054
+ activeInvEl.innerHTML = invHtml || '<div style="font-size:11px;color:var(--text-muted)">No invariants defined</div>';
4055
+
4056
+ // Update state variables if present
4057
+ if (result.world.stateVariables && result.world.stateVariables.length > 0) {
4058
+ // Store as a pseudo-world so sliders render
4059
+ currentWorld = {
4060
+ id: 'custom-world',
4061
+ title: result.world.title,
4062
+ thesis: result.world.thesis,
4063
+ stateVariables: result.world.stateVariables,
4064
+ invariants: result.world.invariants,
4065
+ gates: result.world.gates,
4066
+ };
4067
+ selectWorld('custom-world');
4068
+ } else {
4069
+ // Just set current world reference
4070
+ currentWorld = {
4071
+ id: 'custom-world',
4072
+ title: result.world.title,
4073
+ thesis: result.world.thesis,
4074
+ stateVariables: [],
4075
+ invariants: result.world.invariants,
4076
+ gates: result.world.gates,
4077
+ };
4078
+ }
4079
+ }
4080
+ } catch (err) {
4081
+ uploadStatusEl.textContent = 'Error: ' + err.message;
4082
+ uploadStatusEl.className = 'rule-status error';
4083
+ }
4084
+
4085
+ loadWorldBtn.textContent = 'Load into Runtime';
4086
+ loadWorldBtn.disabled = false;
4087
+ });
4088
+
4089
+ // ============================================
4090
+ // PLAIN-ENGLISH RULE EDITOR
4091
+ // ============================================
4092
+ const ruleInput = document.getElementById('rule-input');
4093
+ const parseRulesBtn = document.getElementById('parse-rules-btn');
4094
+ const parsedRulesEl = document.getElementById('parsed-rules');
4095
+ const ruleStatusEl = document.getElementById('rule-status');
4096
+ let parsedRuleData = [];
4097
+
4098
+ parseRulesBtn.addEventListener('click', async () => {
4099
+ const text = ruleInput.value.trim();
4100
+ if (!text) return;
4101
+
4102
+ parseRulesBtn.disabled = true;
4103
+ parseRulesBtn.textContent = 'Parsing...';
4104
+ ruleStatusEl.textContent = '';
4105
+ ruleStatusEl.className = 'rule-status';
4106
+
4107
+ try {
4108
+ const resp = await fetch('/api/parse-rules', {
4109
+ method: 'POST',
4110
+ headers: { 'Content-Type': 'application/json' },
4111
+ body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
4112
+ });
4113
+ const data = await resp.json();
4114
+
4115
+ if (data.error) {
4116
+ ruleStatusEl.textContent = data.error;
4117
+ ruleStatusEl.className = 'rule-status error';
4118
+ parsedRulesEl.innerHTML = '';
4119
+ parsedRuleData = [];
4120
+ } else {
4121
+ parsedRuleData = data.rules || [];
4122
+ parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
4123
+ const enfType = r.enforcement || 'block';
4124
+ const iconMap = { block: '&#x1F534;', allow: '&#x1F7E2;', modify: '&#x1F535;', warn: '&#x1F7E1;', pause: '&#x1F7E1;' };
4125
+ const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
4126
+ const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
4127
+ const icon = iconMap[enfType] || '&#x1F7E2;';
4128
+ const label = labelMap[enfType] || 'Rule';
4129
+ const effect = effectMap[enfType] || 'Active';
4130
+ return '<div class="parsed-rule enforcement-' + enfType + '">' +
4131
+ '<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
4132
+ '<div class="pr-desc">' + r.description + '</div>' +
4133
+ '<div class="pr-patterns">' + effect + ' &bull; Matches: ' + r.intent_patterns.join(', ') + '</div>' +
4134
+ '</div>';
4135
+ }).join('');
4136
+
4137
+ if (parsedRuleData.length > 0) {
4138
+ const btnLabel = currentWorldSource === 'custom' ? 'Generate World with ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') : 'Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation';
4139
+ parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">' + btnLabel + '</button>';
4140
+ document.getElementById('apply-rules-btn').addEventListener('click', async () => {
4141
+ try {
4142
+ // If in custom rules mode, use base world if selected
4143
+ let worldId = currentWorld ? currentWorld.id : 'trading';
4144
+ if (currentWorldSource === 'custom') {
4145
+ const baseWorld = document.getElementById('custom-base-world').value;
4146
+ if (baseWorld) worldId = baseWorld;
4147
+ }
4148
+
4149
+ const applyResp = await fetch('/api/apply-rules', {
4150
+ method: 'POST',
4151
+ headers: { 'Content-Type': 'application/json' },
4152
+ body: JSON.stringify({ rules: parsedRuleData, worldId }),
4153
+ });
4154
+ const applyData = await applyResp.json();
4155
+ if (applyData.status === 'applied') {
4156
+ ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
4157
+ ruleStatusEl.className = 'rule-status success';
4158
+
4159
+ // Update right panel invariants with custom rules
4160
+ const customName = document.getElementById('custom-world-name');
4161
+ const worldName = (customName && customName.value) ? customName.value : 'Custom World';
4162
+ const customThesis = document.getElementById('custom-world-thesis');
4163
+ const thesis = (customThesis && customThesis.value) ? customThesis.value : 'User-defined governance rules';
4164
+
4165
+ // Show rules in active invariants panel
4166
+ activeInvEl.innerHTML = parsedRuleData.map(r => {
4167
+ const enfType = r.enforcement || 'block';
4168
+ const colorMap = { block: 'var(--red)', allow: 'var(--green)', modify: 'var(--blue)', warn: 'var(--yellow)', pause: 'var(--yellow)' };
4169
+ const color = colorMap[enfType] || 'var(--text-secondary)';
4170
+ return '<div class="inv-item" style="color:' + color + '">[' + r.id + '] ' + r.description + '</div>';
4171
+ }).join('');
4172
+
4173
+ // In custom mode, set a custom world reference
4174
+ if (currentWorldSource === 'custom') {
4175
+ const baseWorld = document.getElementById('custom-base-world').value;
4176
+ if (baseWorld) {
4177
+ selectWorld(baseWorld);
4178
+ } else {
4179
+ currentWorld = { id: 'custom-world', title: worldName, thesis, stateVariables: [], invariants: [], gates: [] };
4180
+ }
4181
+ document.getElementById('world-thesis').textContent = '"' + thesis + '"';
4182
+ }
4183
+ }
4184
+ } catch (err) {
4185
+ ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
4186
+ ruleStatusEl.className = 'rule-status error';
4187
+ }
4188
+ });
4189
+ ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
4190
+ ruleStatusEl.className = 'rule-status success';
4191
+ }
4192
+ }
4193
+ } catch (err) {
4194
+ ruleStatusEl.textContent = 'Error: ' + err.message;
4195
+ ruleStatusEl.className = 'rule-status error';
4196
+ }
4197
+
4198
+ parseRulesBtn.disabled = false;
4199
+ parseRulesBtn.textContent = 'Parse Rules';
4200
+ });
4201
+
4202
+ // ============================================
4203
+ // SESSION TRACKING
4204
+ // ============================================
4205
+
4206
+ let sessionPollInterval = null;
4207
+
4208
+ async function pollSessionStats() {
4209
+ try {
4210
+ const resp = await fetch('/api/session');
4211
+ const data = await resp.json();
4212
+ const el = (id) => document.getElementById(id);
4213
+ if (el('s-total')) el('s-total').textContent = data.evaluations.total;
4214
+ if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
4215
+ if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
4216
+ if (el('s-allowed')) el('s-allowed').textContent = data.evaluations.allowed;
4217
+ if (el('s-agents')) {
4218
+ el('s-agents').textContent = data.agents.length > 0
4219
+ ? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
4220
+ : 'No agents connected yet';
4221
+ }
4222
+ if (el('session-history') && data.historyCount > 0) {
4223
+ el('session-history').textContent = data.historyCount + ' previous session(s) saved for comparison';
4224
+ }
4225
+ } catch {}
4226
+ }
4227
+
4228
+ async function viewSessionReport() {
4229
+ try {
4230
+ const resp = await fetch('/api/session/report');
4231
+ const text = await resp.text();
4232
+ const log = document.getElementById('sim-log');
4233
+ if (log) {
4234
+ const div = document.createElement('div');
4235
+ div.className = 'log-round';
4236
+ 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>';
4237
+ log.prepend(div);
4238
+ log.scrollTop = 0;
4239
+ }
4240
+ } catch (err) { console.error('Failed to load report', err); }
4241
+ }
4242
+
4243
+ async function resetSession() {
4244
+ if (!confirm('Reset session? Current data will be saved for comparison.')) return;
4245
+ try {
4246
+ const resp = await fetch('/api/session/reset', { method: 'POST' });
4247
+ const data = await resp.json();
4248
+ const log = document.getElementById('sim-log');
4249
+ if (log) {
4250
+ const div = document.createElement('div');
4251
+ div.className = 'log-round';
4252
+ div.innerHTML = '<h4 style="color:#fbbf24">Session Reset</h4><div style="font-size:11px;color:#a8a29e">' + data.message + '</div>';
4253
+ log.prepend(div);
4254
+ }
4255
+ pollSessionStats();
4256
+ } catch (err) { console.error('Failed to reset session', err); }
4257
+ }
4258
+
4259
+ async function saveExperiment() {
4260
+ try {
4261
+ const resp = await fetch('/api/session/save', { method: 'POST' });
4262
+ const data = await resp.json();
4263
+ if (data.error) { alert(data.error); return; }
4264
+ const log = document.getElementById('sim-log');
4265
+ if (log) {
4266
+ const div = document.createElement('div');
4267
+ div.className = 'log-round';
4268
+ 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>';
4269
+ log.prepend(div);
4270
+ }
4271
+ } catch (err) { console.error('Failed to save experiment', err); }
4272
+ }
4273
+
4274
+ // Poll session stats every 2 seconds
4275
+ sessionPollInterval = setInterval(pollSessionStats, 2000);
4276
+ pollSessionStats();
4277
+
4278
+ // ============================================
4279
+ // BOOT
4280
+ // ============================================
4281
+ init();
4282
+ <\/script>
4283
+ </body>
4284
+ </html>`;