@neuroverseos/nv-sim 0.1.4 → 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 (47) hide show
  1. package/README.md +346 -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/{reportEngine-BfteK4MN.js → reportEngine-DKWTrP6-.js} +1 -1
  7. package/dist/components/ConstraintsPanel.js +11 -0
  8. package/dist/components/StakeholderBuilder.js +32 -0
  9. package/dist/components/ui/badge.js +24 -0
  10. package/dist/components/ui/button.js +70 -0
  11. package/dist/components/ui/card.js +57 -0
  12. package/dist/components/ui/input.js +44 -0
  13. package/dist/components/ui/label.js +45 -0
  14. package/dist/components/ui/select.js +70 -0
  15. package/dist/engine/aiProvider.js +427 -2
  16. package/dist/engine/auditTrace.js +352 -0
  17. package/dist/engine/behavioralAnalysis.js +605 -0
  18. package/dist/engine/cli.js +1087 -13
  19. package/dist/engine/dynamicsGovernance.js +588 -0
  20. package/dist/engine/fullGovernedLoop.js +367 -0
  21. package/dist/engine/governedSimulation.js +77 -6
  22. package/dist/engine/index.js +41 -1
  23. package/dist/engine/liveVisualizer.js +2787 -360
  24. package/dist/engine/metrics/science.metrics.js +335 -0
  25. package/dist/engine/narrativeInjection.js +55 -0
  26. package/dist/engine/policyEnforcement.js +1611 -0
  27. package/dist/engine/policyEngine.js +799 -0
  28. package/dist/engine/primeRadiant.js +540 -0
  29. package/dist/engine/scenarioCapsule.js +56 -0
  30. package/dist/engine/scenarioComparison.js +463 -0
  31. package/dist/engine/scenarioLibrary.js +17 -0
  32. package/dist/engine/swarmSimulation.js +54 -1
  33. package/dist/engine/worldComparison.js +164 -0
  34. package/dist/engine/worldStorage.js +232 -0
  35. package/dist/index.html +2 -2
  36. package/dist/lib/reasoningEngine.js +290 -0
  37. package/dist/lib/simulationAdapter.js +686 -0
  38. package/dist/lib/swarmParser.js +291 -0
  39. package/dist/lib/types.js +2 -0
  40. package/dist/lib/utils.js +8 -0
  41. package/dist/runtime/govern.js +473 -0
  42. package/dist/runtime/index.js +75 -0
  43. package/dist/runtime/types.js +11 -0
  44. package/package.json +5 -2
  45. package/dist/assets/index-DHKd4rcV.js +0 -338
  46. package/dist/assets/index-SyyA3z3U.css +0 -1
  47. package/dist/assets/swarmSimulation-DHDqjfMa.js +0 -1
@@ -48,6 +48,7 @@ const fs = __importStar(require("fs"));
48
48
  const path = __importStar(require("path"));
49
49
  const swarmSimulation_1 = require("./swarmSimulation");
50
50
  const worldBridge_1 = require("./worldBridge");
51
+ const auditTrace_1 = require("./auditTrace");
51
52
  const narrativeInjection_1 = require("./narrativeInjection");
52
53
  const scenarioLibrary_1 = require("./scenarioLibrary");
53
54
  const liveAdapter_1 = require("./liveAdapter");
@@ -97,9 +98,270 @@ function deleteVariant(variantId) {
97
98
  * The UI sends POST /run-sim with world parameters.
98
99
  * The server runs the simulation and streams results via SSE.
99
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
+ }
100
172
  function startInteractiveServer(port, onReady) {
101
173
  const clients = new Set();
102
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
+ }
103
365
  function broadcast(event) {
104
366
  const data = `data: ${JSON.stringify(event)}\n\n`;
105
367
  for (const client of clients) {
@@ -133,6 +395,18 @@ function startInteractiveServer(port, onReady) {
133
395
  });
134
396
  world.state_variables = updatedVars;
135
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
+ }
136
410
  // Resolve narrative events
137
411
  let narrativeEvents = [];
138
412
  if (config.scenarioId && scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]) {
@@ -156,7 +430,12 @@ function startInteractiveServer(port, onReady) {
156
430
  scenario: resolved.scenario,
157
431
  worldThesis: world.thesis,
158
432
  agents: resolved.stakeholders.map(s => s.id),
159
- invariants: world.invariants.map(inv => ({ id: inv.id, description: inv.description })),
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
+ })),
160
439
  gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
161
440
  narrativeEvents: narrativeEvents.map(e => ({ id: e.id, headline: e.headline, round: e.round, severity: e.severity })),
162
441
  totalRounds: rounds,
@@ -238,7 +517,7 @@ function startInteractiveServer(port, onReady) {
238
517
  }
239
518
  if (req.url === "/api/worlds" && req.method === "GET") {
240
519
  const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
241
- const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis"];
520
+ const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis", "science_research"];
242
521
  const worlds = worldIds.map(id => {
243
522
  try {
244
523
  const r = resolveWorld(id);
@@ -372,47 +651,170 @@ function startInteractiveServer(port, onReady) {
372
651
  // Without guards, everything defaults to ALLOW. These guards define what
373
652
  // simulation actions should be governed.
374
653
  if (!nvWorld.guards) {
375
- nvWorld.guards = {
376
- guards: [
377
- {
378
- id: "sim-panic-actions",
379
- label: "Block panic-driven actions",
380
- description: "Prevents panic selling, aggressive shorting, and other destabilizing actions during high volatility",
381
- category: "structural",
382
- enforcement: "block",
383
- immutable: true,
384
- invariant_ref: nvWorld.invariants[0]?.id,
385
- intent_patterns: ["panic_sell", "panic sell", "panic buy", "panic_buy"],
386
- default_enabled: true,
387
- },
388
- {
389
- id: "sim-excessive-leverage",
390
- label: "Block excessive leverage",
391
- description: "Prevents increasing leverage positions that could amplify cascades",
392
- category: "structural",
393
- enforcement: "block",
394
- immutable: true,
395
- intent_patterns: ["increase_leverage", "increase leverage", "max leverage"],
396
- default_enabled: true,
397
- },
398
- {
399
- id: "sim-aggressive-actions",
400
- label: "Pause aggressive market actions",
401
- description: "Requires review for aggressive buying or shorting that could move markets",
402
- category: "operational",
403
- enforcement: "pause",
404
- immutable: false,
405
- intent_patterns: ["aggressive_buy", "aggressive buy", "aggressive_short", "short"],
406
- default_enabled: true,
407
- },
408
- ],
409
- intent_vocabulary: {
410
- "panic_sell": { label: "Panic Sell", pattern: "panic_sell" },
411
- "increase_leverage": { label: "Increase Leverage", pattern: "increase_leverage" },
412
- "aggressive_buy": { label: "Aggressive Buy", pattern: "aggressive_buy" },
413
- "short": { label: "Short Position", pattern: "short" },
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,
414
670
  },
415
- };
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;
416
818
  }
417
819
  // Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
418
820
  // Omit `direction` — setting it enables execution-intent safety checks (prompt injection
@@ -423,7 +825,7 @@ function startInteractiveServer(port, onReady) {
423
825
  tool: "simulation",
424
826
  scope: `bridge/${payload.actor}`,
425
827
  actionCategory: "execute",
426
- riskLevel: (["panic_sell", "panic_buy", "increase_leverage"].includes(payload.action) ? "high" : "medium"),
828
+ riskLevel: (["panic_sell", "panic_buy", "increase_leverage", "create_post", "repost", "quote_post"].includes(payload.action) ? "high" : "medium"),
427
829
  args: {
428
830
  actor: payload.actor,
429
831
  action: payload.action,
@@ -450,25 +852,60 @@ function startInteractiveServer(port, onReady) {
450
852
  const decision = verdict.status === "BLOCK" ? "BLOCK"
451
853
  : verdict.status === "PAUSE" ? "MODIFY"
452
854
  : "ALLOW";
855
+ // Compute live metrics from cumulative bridge evaluations
856
+ const bridgeMetrics = computeBridgeMetrics(decision);
453
857
  // Broadcast governance event to connected SSE clients
454
858
  broadcast({
455
859
  type: "round",
456
- round: 0,
860
+ round: bridgeMetrics.evalCount,
457
861
  totalRounds: 0,
458
862
  phase: "governed",
459
863
  reactions: [{
460
864
  stakeholder_id: payload.actor,
461
865
  reaction: payload.action,
462
- impact: 0,
866
+ impact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
463
867
  confidence: 0.5,
464
868
  trigger: "bridge",
465
869
  verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
466
870
  }],
467
- avgImpact: 0,
468
- maxVolatility: 0,
871
+ avgImpact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
872
+ maxVolatility: bridgeMetrics.volatility,
469
873
  dynamics: [],
470
874
  interventionCount: decision !== "ALLOW" ? 1 : 0,
471
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
+ });
472
909
  jsonResponse(res, 200, {
473
910
  decision,
474
911
  reason: verdict.reason ?? null,
@@ -478,6 +915,12 @@ function startInteractiveServer(port, onReady) {
478
915
  });
479
916
  }
480
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
+ });
481
924
  // Fail open — return ALLOW on any error
482
925
  jsonResponse(res, 200, {
483
926
  decision: "ALLOW",
@@ -489,6 +932,399 @@ function startInteractiveServer(port, onReady) {
489
932
  }
490
933
  return;
491
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
+ }
492
1328
  // List available live adapters
493
1329
  if (req.url === "/api/adapters" && req.method === "GET") {
494
1330
  const adapters = Object.values(liveAdapter_1.ADAPTER_REGISTRY).map(a => ({
@@ -616,6 +1452,81 @@ function startInteractiveServer(port, onReady) {
616
1452
  }
617
1453
  return;
618
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
+ }
619
1530
  // Serve the interactive dashboard
620
1531
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
621
1532
  res.end(INTERACTIVE_DASHBOARD_HTML);
@@ -632,251 +1543,611 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
632
1543
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
633
1544
  <title>NV-SIM — Scenario Control Platform</title>
634
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
+
635
1594
  * { margin: 0; padding: 0; box-sizing: border-box; }
636
- body { font-family: 'SF Mono', 'Fira Code', monospace; background: #0a0a0a; color: #e0e0e0; overflow: hidden; }
637
- .header { padding: 12px 20px; border-bottom: 1px solid #1a1a1a; display: flex; justify-content: space-between; align-items: center; }
638
- .header h1 { font-size: 15px; color: #fff; }
639
- .header .sub { font-size: 11px; color: #555; margin-left: 12px; }
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); }
640
1602
  .status { font-size: 11px; padding: 3px 10px; border-radius: 10px; }
641
- .status.idle { background: #1a1a2e; color: #818cf8; }
642
- .status.live { background: #052e16; color: #4ade80; animation: pulse 2s infinite; }
643
- .status.complete { background: #1e1b4b; color: #818cf8; }
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); }
644
1606
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
645
1607
 
646
1608
  .layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }
647
1609
 
648
1610
  /* LEFT PANEL — Controls */
649
- .controls { background: #0d0d0d; border-right: 1px solid #1a1a1a; overflow-y: auto; padding: 16px; }
1611
+ .controls { background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px; }
650
1612
  .ctrl-section { margin-bottom: 20px; }
651
- .ctrl-section h3 { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid #1a1a1a; }
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); }
652
1614
 
653
1615
  .ctrl-row { margin-bottom: 12px; }
654
- .ctrl-label { font-size: 11px; color: #888; margin-bottom: 4px; display: flex; justify-content: space-between; }
655
- .ctrl-label .val { color: #ccc; font-weight: 600; }
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; }
656
1618
 
657
- input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: #222; border-radius: 2px; outline: none; }
658
- input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #818cf8; cursor: pointer; }
659
- input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: #818cf8; cursor: pointer; border: none; }
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; }
660
1622
 
661
- select { width: 100%; background: #111; color: #ccc; border: 1px solid #333; padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
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; }
662
1624
 
663
1625
  .toggle-row { display: flex; align-items: center; gap: 8px; }
664
- .toggle { position: relative; width: 36px; height: 20px; background: #333; border-radius: 10px; cursor: pointer; transition: background 0.2s; }
665
- .toggle.on { background: #4ade80; }
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); }
666
1628
  .toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
667
1629
  .toggle.on::after { transform: translateX(16px); }
668
- .toggle-label { font-size: 12px; color: #999; }
1630
+ .toggle-label { font-size: 12px; color: var(--text-secondary); }
669
1631
 
670
1632
  .inject-row { display: flex; gap: 6px; margin-bottom: 6px; }
671
1633
  .inject-row select { flex: 1; }
672
- .inject-row input { width: 50px; background: #111; color: #ccc; border: 1px solid #333; padding: 4px 6px; border-radius: 4px; font-family: inherit; font-size: 12px; text-align: center; }
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; }
673
1635
  .inject-list { margin-bottom: 8px; }
674
- .inject-item { font-size: 11px; color: #888; padding: 3px 6px; background: #111; border-radius: 3px; margin-bottom: 3px; display: flex; justify-content: space-between; }
675
- .inject-item .remove { color: #f87171; cursor: pointer; }
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; }
676
1638
 
677
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; }
678
- .btn-run { background: #4ade80; color: #0a0a0a; }
1640
+ .btn-run { background: var(--green); color: #0a0a0a; }
679
1641
  .btn-run:hover { background: #22c55e; }
680
- .btn-run:disabled { background: #1a3a1a; color: #555; cursor: not-allowed; }
681
- .btn-add { background: #222; color: #818cf8; padding: 6px; font-size: 11px; }
682
- .btn-add:hover { background: #2a2a3a; }
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); }
683
1645
 
684
- .scenario-btn { display: block; width: 100%; padding: 8px 10px; margin-bottom: 4px; background: #111; border: 1px solid #222; border-radius: 4px; color: #ccc; font-family: inherit; font-size: 11px; text-align: left; cursor: pointer; }
685
- .scenario-btn:hover { border-color: #818cf8; background: #151520; }
686
- .scenario-btn .stitle { font-weight: 600; color: #e0e0e0; }
687
- .scenario-btn .sdesc { color: #666; font-size: 10px; margin-top: 2px; }
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; }
688
1650
 
689
1651
  /* Save variant */
690
- .btn-save { background: #1a1a2e; color: #818cf8; margin-top: 0; }
691
- .btn-save:hover { background: #252540; }
692
- .btn-confirm { flex: 1; background: #4ade80; color: #0a0a0a; padding: 7px; font-size: 11px; }
693
- .btn-cancel { flex: 1; background: #222; color: #888; padding: 7px; font-size: 11px; }
694
- .save-input { width: 100%; background: #111; color: #ccc; border: 1px solid #333; padding: 7px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
695
- .save-input:focus { border-color: #818cf8; outline: none; }
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; }
696
1658
 
697
1659
  /* Variant cards */
698
- .variant-card { background: #111; border: 1px solid #222; border-radius: 4px; padding: 8px 10px; margin-bottom: 4px; cursor: pointer; position: relative; }
699
- .variant-card:hover { border-color: #4ade80; background: #0f1f0f; }
700
- .variant-card .vname { font-size: 12px; font-weight: 600; color: #e0e0e0; }
701
- .variant-card .vdesc { font-size: 10px; color: #666; margin-top: 2px; }
702
- .variant-card .vmeta { font-size: 10px; color: #444; margin-top: 4px; }
703
- .variant-card .vmeta .vresult { color: #4ade80; }
704
- .variant-card .vdelete { position: absolute; top: 6px; right: 8px; color: #f87171; font-size: 10px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
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; }
705
1667
  .variant-card:hover .vdelete { opacity: 1; }
706
- .variant-card .vbase { display: inline-block; font-size: 9px; padding: 1px 5px; background: #1a1a2e; color: #818cf8; border-radius: 3px; margin-top: 3px; }
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; }
707
1669
 
708
1670
  /* RIGHT PANEL — Simulation viewer */
709
- .viewer { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
710
- .viewer-top { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: #1a1a1a; }
711
- .viewer-mid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: #1a1a1a; overflow: hidden; }
712
- .viewer-bottom { background: #0a0a0a; border-top: 1px solid #1a1a1a; padding: 12px 16px; max-height: 180px; overflow-y: auto; }
713
-
714
- .vpanel { background: #0a0a0a; padding: 14px; overflow-y: auto; }
715
- .vpanel h2 { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
716
-
717
- /* Metrics */
718
- .metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
719
- .metric-box { background: #111; border: 1px solid #222; border-radius: 6px; padding: 10px; text-align: center; }
720
- .metric-box .value { font-size: 20px; font-weight: 700; color: #fff; }
721
- .metric-box .label { font-size: 10px; color: #555; margin-top: 2px; }
722
- .metric-box.good .value { color: #4ade80; }
723
- .metric-box.bad .value { color: #ef4444; }
724
- .metric-box.warn .value { color: #fbbf24; }
725
-
726
- /* Agent bars */
727
- .agent-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; }
728
- .agent-name { width: 130px; color: #777; flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
729
- .impact-bar-bg { flex: 1; height: 14px; background: #1a1a1a; border-radius: 3px; position: relative; overflow: hidden; }
730
- .impact-bar { height: 100%; border-radius: 3px; transition: width 0.4s ease; position: absolute; top: 0; }
731
- .impact-bar.positive { background: #4ade80; right: 50%; }
732
- .impact-bar.negative { background: #ef4444; left: 50%; }
733
- .impact-val { width: 44px; text-align: right; color: #999; font-size: 10px; }
734
- .center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: #333; }
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); }
735
1739
  .verdict { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-left: 4px; }
736
- .verdict.ALLOW { background: #052e16; color: #4ade80; }
737
- .verdict.BLOCK { background: #2d0606; color: #f87171; }
738
- .verdict.PAUSE { background: #2d2006; color: #fbbf24; }
739
-
740
- /* Chart */
741
- .chart-container { position: relative; height: 100%; min-height: 150px; }
742
- canvas { width: 100% !important; height: 100% !important; }
743
-
744
- /* Simulation Trace */
745
- .trace-round { margin-bottom: 10px; border: 1px solid #1a1a1a; border-radius: 4px; overflow: hidden; }
746
- .trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #111; cursor: pointer; user-select: none; }
747
- .trace-round-header:hover { background: #181818; }
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); }
748
1748
  .trace-phase { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
749
- .trace-phase.baseline { background: #1e293b; color: #60a5fa; }
750
- .trace-phase.governed { background: #052e16; color: #4ade80; }
751
- .trace-round-label { font-size: 11px; color: #ccc; font-weight: 600; }
752
- .trace-round-metrics { margin-left: auto; font-size: 10px; color: #666; display: flex; gap: 10px; }
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; }
753
1753
  .trace-body { padding: 0 10px 8px; }
754
1754
  .trace-body[data-collapsed="true"] { display: none; }
755
1755
  .trace-section { margin-top: 6px; }
756
- .trace-section-label { font-size: 9px; color: #555; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 3px; display: flex; align-items: center; gap: 5px; }
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; }
757
1757
  .trace-section-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
758
1758
  .trace-section-label.event::before { background: #f59e0b; }
759
1759
  .trace-section-label.agents::before { background: #3b82f6; }
760
1760
  .trace-section-label.governance::before { background: #10b981; }
761
- .trace-event-item { font-size: 10px; color: #e2e8f0; padding: 3px 0 3px 11px; border-left: 2px solid #f59e0b; margin-left: 2px; }
1761
+ .trace-event-item { font-size: 10px; color: var(--text-primary); padding: 3px 0 3px 11px; border-left: 2px solid #f59e0b; margin-left: 2px; }
762
1762
  .trace-event-severity { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; margin-left: 4px; }
763
- .trace-event-severity.major, .trace-event-severity.extreme { background: #7f1d1d; color: #fca5a5; }
764
- .trace-event-severity.moderate { background: #713f12; color: #fde68a; }
765
- .trace-event-severity.minor { background: #1e3a2f; color: #86efac; }
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); }
766
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; }
767
- .trace-agent-name { color: #93c5fd; font-weight: 500; min-width: 80px; }
768
- .trace-agent-action { color: #a1a1aa; flex: 1; }
1767
+ .trace-agent-name { color: var(--blue); font-weight: 500; min-width: 80px; }
1768
+ .trace-agent-action { color: var(--text-secondary); flex: 1; }
769
1769
  .trace-agent-impact { font-size: 9px; font-weight: 600; min-width: 36px; text-align: right; }
770
- .trace-agent-impact.positive { color: #4ade80; }
771
- .trace-agent-impact.negative { color: #f87171; }
1770
+ .trace-agent-impact.positive { color: var(--green); }
1771
+ .trace-agent-impact.negative { color: var(--red); }
772
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; }
773
- .trace-gov-rule { font-size: 9px; color: #999; font-family: monospace; }
774
- .trace-gov-reason { color: #d1d5db; flex: 1; }
775
- .trace-dynamics { font-size: 10px; color: #a78bfa; padding: 2px 0 2px 11px; border-left: 2px solid #7c3aed; margin-left: 2px; font-style: italic; }
776
- .trace-arrow { color: #333; font-size: 10px; text-align: center; padding: 2px 0; }
777
- .trace-empty { font-size: 10px; color: #444; font-style: italic; padding: 4px 0; }
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; }
778
1778
 
779
1779
  /* World info */
780
- .world-thesis { font-size: 11px; color: #ccc; font-style: italic; margin-bottom: 8px; }
781
- .inv-item { font-size: 10px; color: #777; padding: 2px 0; }
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; }
782
1799
 
783
1800
  /* Empty state */
784
- .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #333; }
1801
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-faint); }
785
1802
  .empty-state .icon { font-size: 48px; margin-bottom: 12px; }
786
- .empty-state .msg { font-size: 13px; }
787
- .empty-state .hint { font-size: 11px; color: #2a2a2a; margin-top: 6px; }
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; }
788
1805
 
789
1806
  /* System Shift Card */
790
- .system-shift { display: none; margin: 12px 16px; border: 1px solid #1a2e1a; border-radius: 8px; background: #0a0f0a; overflow: hidden; animation: fadeInUp 0.4s ease; }
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; }
791
1808
  .system-shift.visible { display: block; }
792
1809
  @keyframes fadeInUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
793
- .ss-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background: #0d150d; border-bottom: 1px solid #1a2e1a; }
794
- .ss-icon { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 6px #4ade8066; }
795
- .ss-title { font-size: 11px; font-weight: 700; color: #4ade80; text-transform: uppercase; letter-spacing: 1.5px; }
796
- .ss-rule { font-size: 13px; font-weight: 600; color: #e0e0e0; padding: 10px 14px 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; }
797
1814
  .ss-body { padding: 10px 14px 14px; display: grid; gap: 8px; }
798
- .ss-section { background: #111; border: 1px solid #1a1a1a; border-radius: 6px; padding: 10px 12px; }
799
- .ss-section-label { font-size: 9px; font-weight: 700; color: #555; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
800
- .ss-adapt-rate { font-size: 20px; font-weight: 700; color: #4ade80; }
801
- .ss-adapt-desc { font-size: 11px; color: #888; margin-top: 2px; }
802
- .ss-shift-item { font-size: 11px; color: #999; padding: 2px 0; display: flex; align-items: center; gap: 6px; }
803
- .ss-shift-arrow { color: #4ade80; font-weight: 600; }
804
- .ss-pattern-tag { display: inline-block; font-size: 10px; padding: 2px 8px; background: #1a1a2e; color: #818cf8; border-radius: 3px; margin-right: 4px; margin-bottom: 4px; }
805
- .ss-impact-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: #999; padding: 2px 0; }
806
- .ss-impact-delta { color: #4ade80; font-weight: 600; }
807
- .ss-impact-delta.negative { color: #f87171; }
808
- .ss-narrative { font-size: 12px; color: #ccc; line-height: 1.5; font-style: italic; border-left: 2px solid #4ade80; padding-left: 10px; }
809
- .ss-scale { font-size: 10px; color: #555; padding: 0 14px 6px; }
810
- .ss-scale strong { color: #888; }
811
- .ss-flow { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 10px; color: #444; }
812
- .ss-flow-arrow { color: #4ade80; }
813
- .ss-raw-toggle { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: #0d0d0d; border: none; border-top: 1px solid #1a1a1a; color: #555; font-family: inherit; font-size: 10px; cursor: pointer; width: 100%; text-align: left; transition: color 0.2s; }
814
- .ss-raw-toggle:hover { color: #888; }
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); }
815
1832
  .ss-raw-toggle .arrow { transition: transform 0.2s; }
816
1833
  .ss-raw-toggle.open .arrow { transform: rotate(90deg); }
817
1834
  .ss-raw-detail { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
818
1835
  .ss-raw-detail.open { max-height: 200px; overflow-y: auto; }
819
1836
  .ss-raw-list { padding: 6px 12px; }
820
- .ss-raw-item { font-size: 10px; color: #666; padding: 2px 0; display: flex; gap: 6px; }
821
- .ss-raw-item .raw-agent { color: #888; min-width: 100px; }
822
- .ss-raw-item .raw-action { color: #999; flex: 1; }
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; }
823
1840
  .ss-raw-item .raw-verdict { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; }
824
- .ss-raw-item .raw-verdict.BLOCK { background: #2d0606; color: #f87171; }
825
- .ss-raw-item .raw-verdict.MODIFY { background: #2d2006; color: #fbbf24; }
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); }
826
1843
 
827
1844
  /* Integration Quick-Start (in controls panel) */
828
- .integrate-section { background: #0d0d15; border: 1px solid #1a1a2e; border-radius: 6px; padding: 10px 12px; margin-top: 8px; }
829
- .integrate-section h4 { font-size: 10px; color: #818cf8; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
830
- .integrate-code { font-size: 10px; color: #ccc; background: #111; border: 1px solid #222; border-radius: 4px; padding: 8px; overflow-x: auto; white-space: pre; line-height: 1.5; }
831
- .integrate-code .kw { color: #818cf8; }
832
- .integrate-code .str { color: #4ade80; }
833
- .integrate-code .comment { color: #555; }
834
- .integrate-endpoint { font-size: 11px; color: #888; margin-top: 6px; }
835
- .integrate-endpoint code { color: #4ade80; background: #111; padding: 1px 5px; border-radius: 3px; }
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; }
836
1992
  </style>
837
1993
  </head>
838
1994
  <body>
839
1995
  <div class="header">
840
1996
  <div style="display:flex;align-items:center">
841
1997
  <h1>NV-SIM</h1>
842
- <span class="sub">Scenario Control Platform</span>
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>
843
2003
  </div>
844
- <span id="status" class="status idle">Ready</span>
845
2004
  </div>
846
2005
 
847
2006
  <div class="layout">
848
2007
  <!-- LEFT: CONTROLS -->
849
2008
  <div class="controls" id="controls-panel">
850
- <!-- Simulation Engine selector -->
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 -->
851
2018
  <div class="ctrl-section">
852
- <h3>Simulation Engine</h3>
853
- <div class="ctrl-row">
854
- <select id="engine-select">
855
- <option value="nv-sim" selected>NV-SIM (Built-in)</option>
856
- </select>
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>
857
2036
  </div>
858
- <div id="engine-status" style="font-size:10px;color:#555;margin-top:4px"></div>
859
2037
  </div>
860
2038
 
861
- <!-- World selector -->
862
- <div class="ctrl-section">
863
- <h3>World</h3>
864
- <div class="ctrl-row">
865
- <select id="world-select"></select>
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>
866
2059
  </div>
867
- <div id="world-thesis" class="world-thesis"></div>
868
2060
  </div>
869
2061
 
870
- <!-- State variables (dynamic sliders) -->
871
- <div class="ctrl-section" id="state-vars-section" style="display:none">
872
- <h3>World Rules</h3>
873
- <div id="state-vars"></div>
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>
874
2098
  </div>
875
2099
 
876
- <!-- Scenario presets -->
877
- <div class="ctrl-section">
878
- <h3>Scenario Presets</h3>
879
- <div id="scenario-list"></div>
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>
880
2151
  </div>
881
2152
 
882
2153
  <!-- Narrative injection -->
@@ -920,122 +2191,532 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
920
2191
 
921
2192
  <!-- Saved variants -->
922
2193
  <div class="ctrl-section" style="margin-top:16px">
923
- <h3>Saved Variants</h3>
2194
+ <h3>Your Worlds</h3>
924
2195
  <div id="variant-list"><div style="font-size:11px;color:#333">No saved variants yet</div></div>
925
2196
  </div>
926
2197
 
927
2198
  <!-- Integration Quick-Start -->
928
2199
  <div class="ctrl-section" style="margin-top:16px">
929
- <h3>Integrate Your Simulator</h3>
930
- <div class="integrate-section">
931
- <h4>Connect in 3 lines</h4>
932
- <div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate
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())
933
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>
934
2291
  verdict = evaluate(
935
- actor=<span class="str">"agent_1"</span>,
936
- action=<span class="str">"panic_sell"</span>,
937
- world=<span class="str">"trading"</span>
2292
+ actor=<span class="str">"Harry"</span>,
2293
+ action=action,
2294
+ world=<span class="str">"research"</span>
938
2295
  )
939
2296
 
940
- <span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
941
- action = <span class="str">"hold"</span> <span class="comment"># adapted</span></div>
942
- <div class="integrate-endpoint">
943
- Endpoint: <code id="integrate-url">POST /api/evaluate</code>
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>
944
2349
  </div>
945
- <div style="font-size:10px;color:#444;margin-top:6px">
946
- Fail-open · 500ms timeout · Stateless
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>
947
2381
  </div>
948
- <div style="margin-top:8px;font-size:10px">
949
- <span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> replaced
950
- <span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> constrained
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
951
2388
  <span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
952
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>
953
2412
  </div>
954
2413
  </div>
955
2414
  </div>
956
2415
 
957
- <!-- RIGHT: VIEWER -->
958
- <div class="viewer">
959
- <div class="viewer-top">
960
- <div class="vpanel">
961
- <h2>Live Metrics</h2>
962
- <div class="metric-grid">
963
- <div class="metric-box"><div class="value" id="m-stability">--</div><div class="label">Stability</div></div>
964
- <div class="metric-box"><div class="value" id="m-volatility">--</div><div class="label">Volatility</div></div>
965
- <div class="metric-box"><div class="value" id="m-round">--</div><div class="label">Round</div></div>
966
- <div class="metric-box"><div class="value" id="m-interventions">0</div><div class="label">Interventions</div></div>
967
- </div>
968
- </div>
969
- <div class="vpanel">
970
- <h2>World Rules Active</h2>
971
- <div id="active-invariants"></div>
972
- </div>
973
- </div>
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
+ }
974
2693
 
975
- <div class="viewer-mid">
976
- <div class="vpanel" id="agents-panel">
977
- <h2>Agent Impacts</h2>
978
- <div id="agents">
979
- <div class="empty-state"><div class="icon">&gt;_</div><div class="msg">Configure world and run simulation</div><div class="hint">Adjust rules on the left, then press Run</div></div>
980
- </div>
981
- </div>
982
- <div class="vpanel">
983
- <h2>Impact Timeline</h2>
984
- <div class="chart-container"><canvas id="chart"></canvas></div>
985
- </div>
986
- </div>
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
+ }
987
2702
 
988
- <!-- System Shift Card the demo moment -->
989
- <div id="system-shift" class="system-shift">
990
- <div class="ss-header">
991
- <div class="ss-icon"></div>
992
- <span class="ss-title">System Shift</span>
993
- </div>
994
- <div class="ss-rule" id="ss-rule"></div>
995
- <div class="ss-scale" id="ss-scale"></div>
996
- <div class="ss-flow">
997
- <span>Rule</span><span class="ss-flow-arrow">→</span>
998
- <span>Behavioral Shift</span><span class="ss-flow-arrow">→</span>
999
- <span>Emergent Pattern</span><span class="ss-flow-arrow">→</span>
1000
- <span>System Outcome</span>
1001
- </div>
1002
- <div class="ss-body">
1003
- <div class="ss-section">
1004
- <div class="ss-section-label">Behavioral Shift</div>
1005
- <div class="ss-adapt-rate" id="ss-adapt-rate"></div>
1006
- <div class="ss-adapt-desc" id="ss-adapt-desc"></div>
1007
- <div id="ss-shifts"></div>
1008
- </div>
1009
- <div class="ss-section">
1010
- <div class="ss-section-label">What Emerged</div>
1011
- <div id="ss-patterns"></div>
1012
- </div>
1013
- <div class="ss-section">
1014
- <div class="ss-section-label">System Outcome</div>
1015
- <div id="ss-impacts"></div>
1016
- </div>
1017
- <div class="ss-section">
1018
- <div class="ss-section-label">What Actually Happened</div>
1019
- <div class="ss-narrative" id="ss-narrative"></div>
1020
- </div>
1021
- </div>
1022
- <button class="ss-raw-toggle" id="ss-raw-toggle">
1023
- <span class="arrow">▶</span> View raw detail
1024
- </button>
1025
- <div class="ss-raw-detail" id="ss-raw-detail">
1026
- <div class="ss-raw-list" id="ss-raw-list"></div>
1027
- </div>
1028
- </div>
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
+ }
1029
2712
 
1030
- <div class="viewer-bottom">
1031
- <h2 style="font-size:11px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Simulation Trace <span id="trace-source" style="color:#4ade80;font-weight:600;margin-left:6px"></span> <span style="color:#333;font-weight:400">— Events Agents → Rules → Outcomes</span></h2>
1032
- <div id="log"></div>
1033
- </div>
1034
- </div>
1035
- </div>
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
+ }
1036
2719
 
1037
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"><\/script>
1038
- <script>
1039
2720
  // ============================================
1040
2721
  // STATE
1041
2722
  // ============================================
@@ -1045,11 +2726,12 @@ let narratives = {};
1045
2726
  let currentWorld = null;
1046
2727
  let injectedEvents = [];
1047
2728
  let totalInterventions = 0;
1048
- let baselineImpacts = [];
1049
- let governedImpacts = [];
1050
- let chartLabels = [];
1051
- let chart = null;
2729
+ let totalActions = 0;
1052
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;
1053
2735
 
1054
2736
  const statusEl = document.getElementById('status');
1055
2737
  const worldSelect = document.getElementById('world-select');
@@ -1068,6 +2750,79 @@ const activeInvEl = document.getElementById('active-invariants');
1068
2750
  const engineSelect = document.getElementById('engine-select');
1069
2751
  const engineStatusEl = document.getElementById('engine-status');
1070
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
+ }
1071
2826
 
1072
2827
  // ============================================
1073
2828
  // INIT — Load worlds, scenarios, narratives, adapters
@@ -1123,6 +2878,9 @@ async function init() {
1123
2878
  // Load saved variants
1124
2879
  await loadVariants();
1125
2880
 
2881
+ // Populate base world selector for custom rules mode
2882
+ populateBaseWorldSelect();
2883
+
1126
2884
  // Connect SSE
1127
2885
  connectSSE();
1128
2886
  }
@@ -1235,10 +2993,6 @@ runBtn.addEventListener('click', async () => {
1235
2993
 
1236
2994
  // Reset viewer state
1237
2995
  totalInterventions = 0;
1238
- baselineImpacts = [];
1239
- governedImpacts = [];
1240
- chartLabels = [];
1241
- if (chart) { chart.destroy(); chart = null; }
1242
2996
  agentsEl.innerHTML = '';
1243
2997
  logEl.innerHTML = '';
1244
2998
  document.getElementById('m-stability').textContent = '--';
@@ -1306,28 +3060,7 @@ function connectSSE() {
1306
3060
  }
1307
3061
 
1308
3062
  function initChart() {
1309
- if (typeof Chart === 'undefined') return;
1310
- const ctx = document.getElementById('chart');
1311
- chart = new Chart(ctx, {
1312
- type: 'line',
1313
- data: {
1314
- labels: chartLabels,
1315
- datasets: [
1316
- { label: 'Baseline', data: baselineImpacts, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
1317
- { label: 'Governed', data: governedImpacts, borderColor: '#4ade80', backgroundColor: 'rgba(74,222,128,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
1318
- ]
1319
- },
1320
- options: {
1321
- animation: { duration: 400 },
1322
- responsive: true,
1323
- maintainAspectRatio: false,
1324
- plugins: { legend: { labels: { color: '#888', font: { family: 'monospace', size: 10 } } } },
1325
- scales: {
1326
- x: { ticks: { color: '#555' }, grid: { color: '#1a1a1a' } },
1327
- y: { ticks: { color: '#555' }, grid: { color: '#1a1a1a' }, min: -1, max: 1 }
1328
- }
1329
- }
1330
- });
3063
+ // Chart removed no longer needed in outcome-first view
1331
3064
  }
1332
3065
 
1333
3066
  function addLog(msg, cls) {
@@ -1438,6 +3171,84 @@ function renderAgents(reactions) {
1438
3171
  agentsEl.innerHTML = html;
1439
3172
  }
1440
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
+
1441
3252
  function handleEvent(event) {
1442
3253
  if (event.type === 'meta') {
1443
3254
  statusEl.className = 'status live';
@@ -1461,47 +3272,118 @@ function handleEvent(event) {
1461
3272
  if (!narrativeEventsByRound[ev.round]) narrativeEventsByRound[ev.round] = [];
1462
3273
  narrativeEventsByRound[ev.round].push(ev);
1463
3274
  });
1464
- activeInvEl.innerHTML = event.invariants.map(inv =>
1465
- '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
1466
- ).join('') + event.gates.map(g =>
1467
- '<div class="inv-item" style="color:' + (g.severity === 'critical' ? '#f87171' : '#fbbf24') + '">[' + g.id + '] ' + g.label + '</div>'
1468
- ).join('');
1469
- initChart();
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('');
1470
3334
  }
1471
3335
 
1472
3336
  if (event.type === 'round') {
1473
- if (event.phase === 'baseline') {
1474
- chartLabels.push('R' + event.round);
1475
- baselineImpacts.push(event.avgImpact);
1476
- } else {
1477
- governedImpacts.push(event.avgImpact);
1478
- }
1479
- if (chart) chart.update();
3337
+ const isBridge = event.reactions && event.reactions[0] && event.reactions[0].trigger === 'bridge';
1480
3338
 
1481
- renderAgents(event.reactions);
1482
- document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
1483
- document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
1484
- document.getElementById('m-volatility').parentElement.className = 'metric-box ' + (event.maxVolatility > 0.6 ? 'bad' : event.maxVolatility > 0.4 ? 'warn' : 'good');
3339
+ // Record behavior data (no governance display)
3340
+ recordBehavior(event.reactions, event.round);
3341
+ renderAgents(event.reactions); // hidden, kept for audit
1485
3342
 
3343
+ // Track totals
3344
+ totalActions += event.reactions ? event.reactions.length : 0;
1486
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) + '%';
1487
3355
  document.getElementById('m-interventions').textContent = totalInterventions;
1488
3356
 
1489
- // Track system shifts for the card
3357
+ // Track system shifts (data collection, not displayed)
1490
3358
  trackShift(event);
1491
3359
 
1492
- // Render structured trace entry instead of flat log line
3360
+ // Update all visible panels
3361
+ updateAllPanels();
3362
+
3363
+ // Add trace entry for audit
1493
3364
  addTraceRound(event);
1494
3365
  }
1495
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
+
1496
3374
  if (event.type === 'complete') {
1497
3375
  statusEl.className = 'status complete';
1498
3376
  statusEl.textContent = 'COMPLETE';
1499
3377
  const r = event.result;
1500
3378
  if (r.governed) {
3379
+ latestStability = r.governed.metrics.stabilityScore;
3380
+ latestVolatility = r.governed.metrics.maxVolatility || latestVolatility;
1501
3381
  document.getElementById('m-stability').textContent = (r.governed.metrics.stabilityScore * 100).toFixed(0) + '%';
1502
- document.getElementById('m-stability').parentElement.className = 'metric-box ' + (r.governed.metrics.stabilityScore > 0.7 ? 'good' : 'warn');
1503
- addLog('Complete. Governance effectiveness: ' + (r.comparison.governanceEffectiveness * 100).toFixed(0) + '%');
3382
+ addLog('Simulation complete');
1504
3383
  renderSystemShift(r);
3384
+ renderRuleImpacts(r);
3385
+ renderEnforcementClassification(r.enforcementClassification || []);
3386
+ updateAllPanels();
1505
3387
  lastSimResult = {
1506
3388
  stability: r.governed.metrics.stabilityScore,
1507
3389
  volatility: r.governed.metrics.maxVolatility,
@@ -1514,6 +3396,59 @@ function handleEvent(event) {
1514
3396
  }
1515
3397
  }
1516
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
+
1517
3452
  // ============================================
1518
3453
  // SYSTEM SHIFT CARD — The Demo Moment
1519
3454
  // ============================================
@@ -1527,20 +3462,37 @@ const ssImpactsEl = document.getElementById('ss-impacts');
1527
3462
  const ssNarrativeEl = document.getElementById('ss-narrative');
1528
3463
 
1529
3464
  // Raw detail toggle
1530
- document.getElementById('ss-raw-toggle').addEventListener('click', function() {
1531
- this.classList.toggle('open');
1532
- document.getElementById('ss-raw-detail').classList.toggle('open');
1533
- });
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
+ }
1534
3472
 
1535
3473
  let shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
1536
3474
 
1537
3475
  function resetShiftTracker() {
1538
3476
  shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
1539
- ssCard.classList.remove('visible');
3477
+ if (ssCard) ssCard.classList.remove('visible');
1540
3478
  var rawToggle = document.getElementById('ss-raw-toggle');
1541
3479
  var rawDetail = document.getElementById('ss-raw-detail');
1542
3480
  if (rawToggle) rawToggle.classList.remove('open');
1543
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>';
1544
3496
  }
1545
3497
 
1546
3498
  function trackShift(event) {
@@ -1551,6 +3503,18 @@ function trackShift(event) {
1551
3503
  governed.forEach(function(r) {
1552
3504
  const key = r.verdict.status + ': ' + (r.reaction || 'adapted');
1553
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
+ }
1554
3518
  // Store raw governed reactions for detail view
1555
3519
  shiftTracker.rawGoverned.push({
1556
3520
  agent: r.stakeholder_id,
@@ -1571,6 +3535,7 @@ function trackShift(event) {
1571
3535
 
1572
3536
  function renderSystemShift(result) {
1573
3537
  if (shiftTracker.blocks === 0) return;
3538
+ if (!ssCard) return; // System shift card removed from visible UI
1574
3539
 
1575
3540
  var adaptRate = shiftTracker.total > 0 ? Math.round((shiftTracker.blocks / shiftTracker.total) * 100) : 0;
1576
3541
 
@@ -1848,6 +3813,468 @@ handleEvent = function(event) {
1848
3813
  }
1849
3814
  };
1850
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
+
1851
4278
  // ============================================
1852
4279
  // BOOT
1853
4280
  // ============================================