@neuroverseos/nv-sim 0.1.7 → 0.1.10

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 (42) hide show
  1. package/README.md +375 -197
  2. package/connectors/nv_mirofish_wrapper.py +841 -0
  3. package/connectors/nv_scienceclaw_wrapper.py +453 -0
  4. package/dist/adapters/scienceclaw.js +52 -2
  5. package/dist/assets/index-B43_0HyO.css +1 -0
  6. package/dist/assets/index-CdghpsS8.js +595 -0
  7. package/dist/assets/{reportEngine-BVdQ2_nW.js → reportEngine-CYSZfooa.js} +1 -1
  8. package/dist/connectors/nv-scienceclaw-post.js +376 -0
  9. package/dist/engine/aiProvider.js +82 -3
  10. package/dist/engine/analyzer.js +12 -24
  11. package/dist/engine/chaosEngine.js +3 -9
  12. package/dist/engine/cli.js +123 -218
  13. package/dist/engine/dynamicsGovernance.js +4 -0
  14. package/dist/engine/fullGovernedLoop.js +16 -1
  15. package/dist/engine/goalEngine.js +3 -4
  16. package/dist/engine/governance.js +18 -0
  17. package/dist/engine/index.js +19 -29
  18. package/dist/engine/intentTranslator.js +281 -0
  19. package/dist/engine/liveAdapter.js +100 -18
  20. package/dist/engine/liveVisualizer.js +2656 -866
  21. package/dist/engine/narrativeInjection.js +78 -89
  22. package/dist/engine/policyEngine.js +171 -58
  23. package/dist/engine/primeRadiant.js +2 -8
  24. package/dist/engine/reasoningEngine.js +2 -7
  25. package/dist/engine/scenarioCapsule.js +77 -133
  26. package/dist/engine/scenarioLibrary.js +52 -131
  27. package/dist/engine/swarmSimulation.js +1 -9
  28. package/dist/engine/worldBridge.js +22 -8
  29. package/dist/engine/worldComparison.js +12 -25
  30. package/dist/index.html +2 -2
  31. package/dist/lib/reasoningEngine.js +17 -1
  32. package/dist/lib/simulationAdapter.js +11 -11
  33. package/dist/lib/swarmParser.js +1 -1
  34. package/dist/runtime/govern.js +160 -7
  35. package/dist/runtime/index.js +1 -4
  36. package/dist/runtime/types.js +91 -0
  37. package/package.json +23 -6
  38. package/dist/adapters/mirofish.js +0 -461
  39. package/dist/assets/index-CHmUN8s0.js +0 -532
  40. package/dist/assets/index-DWgMnB7I.css +0 -1
  41. package/dist/assets/mirotir-logo-DUexumBH.svg +0 -185
  42. package/dist/engine/mirofish.js +0 -295
@@ -46,6 +46,7 @@ exports.startInteractiveServer = startInteractiveServer;
46
46
  const http = __importStar(require("http"));
47
47
  const fs = __importStar(require("fs"));
48
48
  const path = __importStar(require("path"));
49
+ const child_process_1 = require("child_process");
49
50
  const swarmSimulation_1 = require("./swarmSimulation");
50
51
  const worldBridge_1 = require("./worldBridge");
51
52
  const auditTrace_1 = require("./auditTrace");
@@ -180,12 +181,96 @@ function startInteractiveServer(port, onReady) {
180
181
  let currentSession = {
181
182
  id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
182
183
  startedAt: new Date().toISOString(),
183
- world: "trading",
184
+ world: "social_simulation",
184
185
  guardCount: 0,
185
186
  evaluations: [],
186
187
  };
187
188
  // Session snapshots for multi-run comparison
188
189
  const sessionHistory = [];
190
+ // ── AI API Key (BYOK — Bring Your Own Key) ──
191
+ // Users configure their AI provider key through the dashboard.
192
+ // Stored in memory for the session; can also be set via environment variables.
193
+ let userAIConfig = {
194
+ provider: (process.env.ANTHROPIC_API_KEY ? "anthropic" : process.env.OPENAI_API_KEY ? "openai" : "anthropic"),
195
+ apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? "",
196
+ baseUrl: process.env.OPENAI_BASE_URL,
197
+ model: process.env.OPENAI_MODEL,
198
+ };
199
+ // ── Active Bridge Process ──
200
+ // Tracks a spawned simulator (e.g., ScienceClaw) launched from the visualizer
201
+ let activeBridgeProcess = null;
202
+ // ── Bridge Metrics Tracker ──
203
+ // Computes meaningful live metrics from external /api/evaluate calls
204
+ let bridgeEvalCount = 0;
205
+ let bridgeBlockCount = 0;
206
+ let bridgeModifyCount = 0;
207
+ let bridgeRewardCount = 0;
208
+ let bridgePenalizeCount = 0;
209
+ function computeBridgeMetrics(decision) {
210
+ bridgeEvalCount++;
211
+ if (decision === "BLOCK")
212
+ bridgeBlockCount++;
213
+ if (decision === "MODIFY")
214
+ bridgeModifyCount++;
215
+ if (decision === "REWARD")
216
+ bridgeRewardCount++;
217
+ if (decision === "PENALIZE")
218
+ bridgePenalizeCount++;
219
+ const totalInterventions = bridgeBlockCount + bridgeModifyCount + bridgePenalizeCount;
220
+ // Stability = ratio of non-blocked actions (higher = more stable)
221
+ const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount - bridgePenalizeCount) / bridgeEvalCount : 1;
222
+ // Volatility = ratio of interventions (blocks + modifies + penalizes) — more interventions = more volatile
223
+ const volatility = bridgeEvalCount > 0 ? totalInterventions / bridgeEvalCount : 0;
224
+ return { stability, volatility, totalInterventions, evalCount: bridgeEvalCount };
225
+ }
226
+ // ── Agent Behavior State Registry ──
227
+ // Tracks per-agent incentive state: cooldowns, influence, rewards/penalties
228
+ // Uses @neuroverseos/governance decision-flow-engine when available, falls back to local tracking
229
+ const agentBehaviorStates = new Map();
230
+ function getOrCreateAgentState(agentId) {
231
+ if (!agentBehaviorStates.has(agentId)) {
232
+ agentBehaviorStates.set(agentId, {
233
+ agentId,
234
+ cooldownRemaining: 0,
235
+ influence: 1.0,
236
+ rewardMultiplier: 1.0,
237
+ totalPenalties: 0,
238
+ totalRewards: 0,
239
+ consequenceHistory: [],
240
+ rewardHistory: [],
241
+ });
242
+ }
243
+ return agentBehaviorStates.get(agentId);
244
+ }
245
+ function localApplyConsequence(agentId, consequence, ruleId) {
246
+ const state = getOrCreateAgentState(agentId);
247
+ state.totalPenalties++;
248
+ state.consequenceHistory.push({ ruleId, consequence, appliedAt: Date.now() });
249
+ if (consequence.type === "freeze" || consequence.type === "cooldown") {
250
+ state.cooldownRemaining = Math.max(state.cooldownRemaining, consequence.rounds ?? 1);
251
+ }
252
+ if (consequence.type === "reduce_influence") {
253
+ state.influence = Math.max(0, state.influence - (consequence.magnitude ?? 0.2));
254
+ }
255
+ return state;
256
+ }
257
+ function localApplyReward(agentId, reward, ruleId) {
258
+ const state = getOrCreateAgentState(agentId);
259
+ state.totalRewards++;
260
+ state.rewardHistory.push({ ruleId, reward, appliedAt: Date.now() });
261
+ if (reward.type === "boost_influence") {
262
+ state.influence = Math.min(2.0, state.influence + (reward.magnitude ?? 0.1));
263
+ }
264
+ if (reward.type === "weight_increase") {
265
+ state.rewardMultiplier = Math.min(3.0, state.rewardMultiplier + (reward.magnitude ?? 0.1));
266
+ }
267
+ return state;
268
+ }
269
+ function localTickAgentStates() {
270
+ for (const [, state] of agentBehaviorStates) {
271
+ state.cooldownRemaining = Math.max(0, state.cooldownRemaining - 1);
272
+ }
273
+ }
189
274
  function synthesizeSessionReport() {
190
275
  // Gather all sessions to report on (history + current if it has data)
191
276
  const allSessions = [
@@ -499,7 +584,7 @@ function startInteractiveServer(port, onReady) {
499
584
  }
500
585
  if (req.url === "/api/worlds" && req.method === "GET") {
501
586
  const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
502
- const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis"];
587
+ const worldIds = ["social_simulation", "science_research"];
503
588
  const worlds = worldIds.map(id => {
504
589
  try {
505
590
  const r = resolveWorld(id);
@@ -536,24 +621,47 @@ function startInteractiveServer(port, onReady) {
536
621
  try {
537
622
  const body = await readBody(req);
538
623
  const payload = JSON.parse(body);
539
- if (!payload.name || !payload.baseWorld) {
540
- jsonResponse(res, 400, { error: "name and baseWorld are required" });
624
+ if (!payload.name) {
625
+ jsonResponse(res, 400, { error: "name is required" });
541
626
  return;
542
627
  }
543
628
  const id = slugify(payload.name);
544
- const variant = {
545
- id,
546
- name: payload.name,
547
- description: payload.description ?? "",
548
- baseWorld: payload.baseWorld,
549
- stateOverrides: payload.stateOverrides ?? {},
550
- events: payload.events ?? [],
551
- rounds: payload.rounds ?? 5,
552
- createdAt: new Date().toISOString(),
553
- lastResult: payload.lastResult,
554
- };
555
- const filepath = saveVariant(variant);
556
- jsonResponse(res, 200, { status: "saved", variant, filepath });
629
+ // Support both old WorldVariant format and new SimulationRules format
630
+ if (payload.thesis !== undefined) {
631
+ // New SimulationRules format — complete snapshot
632
+ const rules = {
633
+ id,
634
+ name: payload.name,
635
+ description: payload.description ?? "",
636
+ thesis: payload.thesis,
637
+ bridgeId: payload.bridgeId,
638
+ rules: payload.rules ?? [],
639
+ worldDefinition: payload.worldDefinition,
640
+ stateOverrides: payload.stateOverrides ?? {},
641
+ createdAt: new Date().toISOString(),
642
+ lastResult: payload.lastResult,
643
+ };
644
+ ensureVariantsDir();
645
+ const filepath = path.join(getVariantsDir(), `${id}.json`);
646
+ fs.writeFileSync(filepath, JSON.stringify(rules, null, 2), "utf-8");
647
+ jsonResponse(res, 200, { status: "saved", variant: rules, filepath });
648
+ }
649
+ else {
650
+ // Legacy WorldVariant format
651
+ const variant = {
652
+ id,
653
+ name: payload.name,
654
+ description: payload.description ?? "",
655
+ baseWorld: payload.baseWorld ?? "unknown",
656
+ stateOverrides: payload.stateOverrides ?? {},
657
+ events: payload.events ?? [],
658
+ rounds: payload.rounds ?? 5,
659
+ createdAt: new Date().toISOString(),
660
+ lastResult: payload.lastResult,
661
+ };
662
+ const filepath = saveVariant(variant);
663
+ jsonResponse(res, 200, { status: "saved", variant, filepath });
664
+ }
557
665
  }
558
666
  catch (err) {
559
667
  jsonResponse(res, 400, { error: "Invalid request body" });
@@ -602,7 +710,7 @@ function startInteractiveServer(port, onReady) {
602
710
  return;
603
711
  }
604
712
  // Resolve world for evaluation
605
- const worldId = payload.world ?? "trading";
713
+ const worldId = payload.world ?? "social_simulation";
606
714
  let nvWorld;
607
715
  try {
608
716
  const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
@@ -671,7 +779,7 @@ function startInteractiveServer(port, onReady) {
671
779
  default_enabled: true,
672
780
  },
673
781
  ];
674
- // Social media guards for MiroFish/OASIS agent actions.
782
+ // Social media guards for OASIS agent actions.
675
783
  // These govern what AI agents can do on simulated social platforms.
676
784
  const socialGuards = [
677
785
  {
@@ -741,6 +849,8 @@ function startInteractiveServer(port, onReady) {
741
849
  immutable: false,
742
850
  intent_patterns: cg.intent_patterns,
743
851
  default_enabled: true,
852
+ ...(cg.consequence ? { consequence: cg.consequence } : {}),
853
+ ...(cg.reward ? { reward: cg.reward } : {}),
744
854
  });
745
855
  // Add patterns to vocabulary
746
856
  for (const pat of cg.intent_patterns) {
@@ -798,6 +908,90 @@ function startInteractiveServer(port, onReady) {
798
908
  });
799
909
  return;
800
910
  }
911
+ // ── Local incentive guard check ──
912
+ // Custom guards with enforcement=penalize or enforcement=reward need local matching
913
+ // because the fallback evaluateScenarioGuard only knows ALLOW/BLOCK/PAUSE.
914
+ // Also check state-based conditions (e.g. low confidence → penalize publishing).
915
+ const actionLower = payload.action.toLowerCase().replace(/_/g, " ");
916
+ const stateFacts = payload.state ?? {};
917
+ for (const cg of customGuards) {
918
+ if (cg.enforcement !== "penalize" && cg.enforcement !== "reward")
919
+ continue;
920
+ // Check if action matches any intent pattern
921
+ const matched = cg.intent_patterns.some(pat => {
922
+ const p = pat.toLowerCase();
923
+ return actionLower.includes(p) || p.includes(actionLower) || payload.action.toLowerCase() === p;
924
+ });
925
+ if (!matched)
926
+ continue;
927
+ // For penalize: check if state conditions indicate violation
928
+ // For reward: check if state conditions indicate good behavior
929
+ const isViolation = cg.enforcement === "penalize";
930
+ const isReward = cg.enforcement === "reward";
931
+ if (isViolation) {
932
+ const consequence = cg.consequence ?? { type: "freeze", rounds: 1, description: cg.description };
933
+ const v = {
934
+ status: "PENALIZE",
935
+ reason: cg.description,
936
+ ruleId: cg.id,
937
+ consequence: consequence,
938
+ };
939
+ // Record and respond
940
+ const agState = getOrCreateAgentState(payload.actor);
941
+ localApplyConsequence(payload.actor, consequence, cg.id);
942
+ const metrics = computeBridgeMetrics("PENALIZE");
943
+ broadcast({
944
+ type: "round", round: metrics.evalCount, totalRounds: 0, phase: "governed",
945
+ reactions: [{
946
+ stakeholder_id: payload.actor, reaction: payload.action, impact: -0.6, confidence: 0.5,
947
+ trigger: "bridge",
948
+ verdict: { status: "PENALIZE", reason: v.reason, ruleId: cg.id, incentive: { type: "penalize", magnitude: consequence.magnitude ?? 1, cooldownRounds: consequence.rounds ?? 0, description: consequence.description }, agentState: { cooldown: agState.cooldownRemaining, influence: agState.influence, penalties: agState.totalPenalties, rewards: agState.totalRewards } },
949
+ }],
950
+ avgImpact: -0.6, maxVolatility: metrics.volatility, dynamics: [], interventionCount: 1,
951
+ });
952
+ currentSession.evaluations.push({
953
+ actor: payload.actor, action: payload.action, decision: "PENALIZE",
954
+ reason: cg.description, ruleId: cg.id, world: payload.world ?? currentSession.world,
955
+ timestamp: Date.now(), payload: payload.payload,
956
+ incentive: { type: "penalize", magnitude: consequence.magnitude ?? 1, cooldownRounds: consequence.rounds ?? 0, description: consequence.description },
957
+ });
958
+ jsonResponse(res, 200, {
959
+ decision: "PENALIZE", reason: cg.description, rule_id: cg.id,
960
+ evidence: null, modified_action: null,
961
+ consequence, reward: null,
962
+ agent_state: { cooldown_remaining: agState.cooldownRemaining, influence: agState.influence, reward_multiplier: agState.rewardMultiplier, total_penalties: agState.totalPenalties, total_rewards: agState.totalRewards },
963
+ });
964
+ return;
965
+ }
966
+ if (isReward) {
967
+ const reward = cg.reward ?? { type: "boost_influence", magnitude: 0.1, description: cg.description };
968
+ const agState = getOrCreateAgentState(payload.actor);
969
+ localApplyReward(payload.actor, reward, cg.id);
970
+ const metrics = computeBridgeMetrics("REWARD");
971
+ broadcast({
972
+ type: "round", round: metrics.evalCount, totalRounds: 0, phase: "governed",
973
+ reactions: [{
974
+ stakeholder_id: payload.actor, reaction: payload.action, impact: 0.5, confidence: 0.5,
975
+ trigger: "bridge",
976
+ verdict: { status: "REWARD", reason: cg.description, ruleId: cg.id, incentive: { type: "reward", magnitude: reward.magnitude ?? 0.1, cooldownRounds: 0, description: reward.description }, agentState: { cooldown: agState.cooldownRemaining, influence: agState.influence, penalties: agState.totalPenalties, rewards: agState.totalRewards } },
977
+ }],
978
+ avgImpact: 0.5, maxVolatility: metrics.volatility, dynamics: [], interventionCount: 0,
979
+ });
980
+ currentSession.evaluations.push({
981
+ actor: payload.actor, action: payload.action, decision: "REWARD",
982
+ reason: cg.description, ruleId: cg.id, world: payload.world ?? currentSession.world,
983
+ timestamp: Date.now(), payload: payload.payload,
984
+ incentive: { type: "reward", magnitude: reward.magnitude ?? 0.1, cooldownRounds: 0, description: reward.description },
985
+ });
986
+ jsonResponse(res, 200, {
987
+ decision: "REWARD", reason: cg.description, rule_id: cg.id,
988
+ evidence: null, modified_action: null,
989
+ consequence: null, reward,
990
+ agent_state: { cooldown_remaining: agState.cooldownRemaining, influence: agState.influence, reward_multiplier: agState.rewardMultiplier, total_penalties: agState.totalPenalties, total_rewards: agState.totalRewards },
991
+ });
992
+ return;
993
+ }
994
+ }
801
995
  // Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
802
996
  // Omit `direction` — setting it enables execution-intent safety checks (prompt injection
803
997
  // detection) which falsely flag financial terms like "buy" and "sell". Bridge actions are
@@ -812,6 +1006,7 @@ function startInteractiveServer(port, onReady) {
812
1006
  actor: payload.actor,
813
1007
  action: payload.action,
814
1008
  ...(payload.payload ?? {}),
1009
+ ...(payload.state ? { state: payload.state } : {}),
815
1010
  },
816
1011
  };
817
1012
  // Evaluate directly via the governance module
@@ -830,36 +1025,116 @@ function startInteractiveServer(port, onReady) {
830
1025
  stakeholders: [{ id: payload.actor, description: payload.actor, disposition: "neutral", priorities: [] }],
831
1026
  }, nvWorld, { trace: true, level: "standard" });
832
1027
  }
833
- // Map verdict to bridge protocol
1028
+ // Map verdict to bridge protocol — includes REWARD/PENALIZE from governance engine
834
1029
  const decision = verdict.status === "BLOCK" ? "BLOCK"
835
1030
  : verdict.status === "PAUSE" ? "MODIFY"
836
- : "ALLOW";
1031
+ : verdict.status === "REWARD" ? "REWARD"
1032
+ : verdict.status === "PENALIZE" ? "PENALIZE"
1033
+ : "ALLOW";
1034
+ // ── Apply incentive effects to agent behavior state ──
1035
+ // Uses @neuroverseos/governance decision-flow-engine types (consequence/reward)
1036
+ let agentState = getOrCreateAgentState(payload.actor);
1037
+ // Check if agent is in cooldown — if so, block regardless of verdict
1038
+ let effectiveDecision = decision;
1039
+ if (agentState.cooldownRemaining > 0 && decision !== "REWARD") {
1040
+ effectiveDecision = "PENALIZE";
1041
+ // Override verdict reason to explain cooldown
1042
+ verdict.reason = `Agent frozen: ${agentState.cooldownRemaining} round(s) remaining on cooldown`;
1043
+ }
1044
+ // Apply consequence (PENALIZE verdict)
1045
+ if (verdict.status === "PENALIZE" && verdict.consequence) {
1046
+ try {
1047
+ const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
1048
+ if (nv.applyConsequence) {
1049
+ agentState = nv.applyConsequence(agentState, verdict.consequence, verdict.ruleId ?? "unknown");
1050
+ agentBehaviorStates.set(payload.actor, agentState);
1051
+ }
1052
+ else {
1053
+ localApplyConsequence(payload.actor, verdict.consequence, verdict.ruleId ?? "unknown");
1054
+ }
1055
+ }
1056
+ catch {
1057
+ localApplyConsequence(payload.actor, verdict.consequence, verdict.ruleId ?? "unknown");
1058
+ }
1059
+ }
1060
+ else if (verdict.status === "PENALIZE") {
1061
+ // Fallback: no consequence payload, apply default freeze
1062
+ localApplyConsequence(payload.actor, { type: "freeze", rounds: 1, description: verdict.reason ?? "Penalized" }, verdict.ruleId ?? "unknown");
1063
+ }
1064
+ // Apply reward (REWARD verdict)
1065
+ if (verdict.status === "REWARD" && verdict.reward) {
1066
+ try {
1067
+ const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
1068
+ if (nv.applyReward) {
1069
+ agentState = nv.applyReward(agentState, verdict.reward, verdict.ruleId ?? "unknown");
1070
+ agentBehaviorStates.set(payload.actor, agentState);
1071
+ }
1072
+ else {
1073
+ localApplyReward(payload.actor, verdict.reward, verdict.ruleId ?? "unknown");
1074
+ }
1075
+ }
1076
+ catch {
1077
+ localApplyReward(payload.actor, verdict.reward, verdict.ruleId ?? "unknown");
1078
+ }
1079
+ }
1080
+ else if (verdict.status === "REWARD") {
1081
+ // Fallback: no reward payload, apply default boost
1082
+ localApplyReward(payload.actor, { type: "boost_influence", magnitude: 0.1, description: verdict.reason ?? "Rewarded" }, verdict.ruleId ?? "unknown");
1083
+ }
1084
+ // Build incentive metadata for response (normalized from consequence/reward)
1085
+ const incentive = verdict.consequence
1086
+ ? { type: "penalize", magnitude: verdict.consequence.magnitude ?? 1, cooldownRounds: verdict.consequence.rounds ?? 0, description: verdict.consequence.description }
1087
+ : verdict.reward
1088
+ ? { type: "reward", magnitude: verdict.reward.magnitude ?? 1, cooldownRounds: 0, description: verdict.reward.description }
1089
+ : undefined;
1090
+ // Compute live metrics from cumulative bridge evaluations
1091
+ const bridgeMetrics = computeBridgeMetrics(effectiveDecision);
1092
+ // Impact scoring — factor in agent influence level
1093
+ const baseImpact = effectiveDecision === "BLOCK" ? -0.8
1094
+ : effectiveDecision === "PENALIZE" ? -0.6
1095
+ : effectiveDecision === "MODIFY" ? -0.3
1096
+ : effectiveDecision === "REWARD" ? 0.5
1097
+ : 0.1;
1098
+ const impactForDecision = baseImpact * agentState.influence;
837
1099
  // Broadcast governance event to connected SSE clients
838
1100
  broadcast({
839
1101
  type: "round",
840
- round: 0,
1102
+ round: bridgeMetrics.evalCount,
841
1103
  totalRounds: 0,
842
1104
  phase: "governed",
843
1105
  reactions: [{
844
1106
  stakeholder_id: payload.actor,
845
1107
  reaction: payload.action,
846
- impact: 0,
847
- confidence: 0.5,
1108
+ impact: impactForDecision,
1109
+ confidence: 0.5 * agentState.rewardMultiplier,
848
1110
  trigger: "bridge",
849
- verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
1111
+ verdict: {
1112
+ status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId,
1113
+ ...(incentive ? { incentive } : {}),
1114
+ agentState: { cooldown: agentState.cooldownRemaining, influence: agentState.influence, penalties: agentState.totalPenalties, rewards: agentState.totalRewards },
1115
+ },
850
1116
  }],
851
- avgImpact: 0,
852
- maxVolatility: 0,
1117
+ avgImpact: impactForDecision,
1118
+ maxVolatility: bridgeMetrics.volatility,
853
1119
  dynamics: [],
854
- interventionCount: decision !== "ALLOW" ? 1 : 0,
1120
+ interventionCount: effectiveDecision !== "ALLOW" && effectiveDecision !== "REWARD" ? 1 : 0,
1121
+ });
1122
+ // Also broadcast a stability update so the metric refreshes live
1123
+ broadcast({
1124
+ type: "bridge_metrics",
1125
+ stability: bridgeMetrics.stability,
1126
+ volatility: bridgeMetrics.volatility,
1127
+ evalCount: bridgeMetrics.evalCount,
1128
+ totalInterventions: bridgeMetrics.totalInterventions,
855
1129
  });
856
1130
  // Record in session
857
1131
  currentSession.evaluations.push({
858
1132
  actor: payload.actor, action: payload.action,
859
- decision: decision,
1133
+ decision: effectiveDecision,
860
1134
  reason: verdict.reason ?? "", ruleId: verdict.ruleId ?? null,
861
1135
  world: payload.world ?? currentSession.world,
862
1136
  timestamp: Date.now(), payload: payload.payload,
1137
+ incentive,
863
1138
  });
864
1139
  currentSession.guardCount = customGuards.length + (nvWorld.guards?.guards?.length ?? 0);
865
1140
  // Persist to audit trail on disk
@@ -867,23 +1142,32 @@ function startInteractiveServer(port, onReady) {
867
1142
  agent: payload.actor,
868
1143
  action: payload.action,
869
1144
  actionType: payload.payload?.type ?? "unknown",
870
- verdict: decision,
1145
+ verdict: effectiveDecision,
871
1146
  reason: verdict.reason ?? "",
872
1147
  confidence: verdict.confidence ?? 0.5,
873
1148
  rulesFired: verdict.ruleId ? [{
874
1149
  id: verdict.ruleId,
875
1150
  description: verdict.reason ?? "",
876
- effect: decision === "BLOCK" ? "blocked" : decision === "MODIFY" ? "dampened" : "monitored",
877
- impactReduction: decision === "BLOCK" ? 1 : decision === "MODIFY" ? 0.5 : 0,
1151
+ effect: effectiveDecision === "BLOCK" ? "blocked" : effectiveDecision === "PENALIZE" ? "blocked" : effectiveDecision === "MODIFY" ? "dampened" : effectiveDecision === "REWARD" ? "monitored" : "monitored",
1152
+ impactReduction: effectiveDecision === "BLOCK" ? 1 : effectiveDecision === "PENALIZE" ? 0.8 : effectiveDecision === "MODIFY" ? 0.5 : 0,
878
1153
  }] : [],
879
1154
  worldState: payload.world ?? currentSession.world,
880
1155
  });
881
1156
  jsonResponse(res, 200, {
882
- decision,
1157
+ decision: effectiveDecision,
883
1158
  reason: verdict.reason ?? null,
884
1159
  rule_id: verdict.ruleId ?? null,
885
1160
  evidence: verdict.evidence ?? null,
886
- modified_action: decision === "MODIFY" ? payload.payload : null,
1161
+ modified_action: effectiveDecision === "MODIFY" ? payload.payload : null,
1162
+ consequence: verdict.consequence ?? null,
1163
+ reward: verdict.reward ?? null,
1164
+ agent_state: {
1165
+ cooldown_remaining: agentState.cooldownRemaining,
1166
+ influence: agentState.influence,
1167
+ reward_multiplier: agentState.rewardMultiplier,
1168
+ total_penalties: agentState.totalPenalties,
1169
+ total_rewards: agentState.totalRewards,
1170
+ },
887
1171
  });
888
1172
  }
889
1173
  catch (err) {
@@ -923,6 +1207,59 @@ function startInteractiveServer(port, onReady) {
923
1207
  }
924
1208
  return;
925
1209
  }
1210
+ // Generate a full governed world from plain-English rules
1211
+ // Returns a complete world: thesis, state variables, invariants, gates
1212
+ if (req.url === "/api/generate-world" && req.method === "POST") {
1213
+ try {
1214
+ const body = await readBody(req);
1215
+ const payload = JSON.parse(body);
1216
+ if (!payload.text) {
1217
+ jsonResponse(res, 400, { error: "text is required" });
1218
+ return;
1219
+ }
1220
+ const { parseRulesFromText, policyToWorld } = await Promise.resolve().then(() => __importStar(require("./policyEngine")));
1221
+ const parsed = parseRulesFromText(payload.text);
1222
+ const worldDef = policyToWorld(parsed);
1223
+ const worldName = payload.name || "Custom World";
1224
+ const worldId = "custom-" + worldName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
1225
+ // Apply as the active world
1226
+ customGuards.length = 0;
1227
+ for (let i = 0; i < parsed.rules.length; i++) {
1228
+ const rule = parsed.rules[i];
1229
+ customGuards.push({
1230
+ id: worldDef.invariants[i]?.id ?? `custom-rule-${i}`,
1231
+ label: rule.description,
1232
+ description: rule.description,
1233
+ category: "custom",
1234
+ enforcement: (rule.category === "prohibit" || rule.category === "circuit_breaker") ? "block" : rule.category === "limit" ? "block" : "allow",
1235
+ immutable: false,
1236
+ intent_patterns: [...rule.keywords],
1237
+ default_enabled: true,
1238
+ });
1239
+ }
1240
+ jsonResponse(res, 200, {
1241
+ status: "generated",
1242
+ world: {
1243
+ id: worldId,
1244
+ title: worldName,
1245
+ thesis: worldDef.thesis,
1246
+ stateVariables: worldDef.state_variables,
1247
+ invariants: worldDef.invariants,
1248
+ gates: worldDef.gates ?? [],
1249
+ },
1250
+ parsed: {
1251
+ total: parsed.summary.total,
1252
+ enforced: parsed.summary.enforced,
1253
+ advisory: parsed.summary.advisory,
1254
+ },
1255
+ rulesApplied: customGuards.length,
1256
+ });
1257
+ }
1258
+ catch (err) {
1259
+ jsonResponse(res, 400, { error: err?.message ?? "Invalid request" });
1260
+ }
1261
+ return;
1262
+ }
926
1263
  // Apply parsed rules to the active governance context
927
1264
  if (req.url === "/api/apply-rules" && req.method === "POST") {
928
1265
  try {
@@ -944,6 +1281,8 @@ function startInteractiveServer(port, onReady) {
944
1281
  immutable: false,
945
1282
  intent_patterns: rule.intent_patterns,
946
1283
  default_enabled: true,
1284
+ ...(rule.consequence ? { consequence: rule.consequence } : {}),
1285
+ ...(rule.reward ? { reward: rule.reward } : {}),
947
1286
  });
948
1287
  }
949
1288
  jsonResponse(res, 200, {
@@ -1050,7 +1389,7 @@ function startInteractiveServer(port, onReady) {
1050
1389
  // Exports the current world configuration (base world + custom rules + overrides) as a world file
1051
1390
  if (req.url === "/api/export-world" && req.method === "GET") {
1052
1391
  const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
1053
- const worldId = currentSession.world || "trading";
1392
+ const worldId = currentSession.world || "social_simulation";
1054
1393
  let baseWorld;
1055
1394
  try {
1056
1395
  baseWorld = resolveWorld(worldId);
@@ -1088,6 +1427,8 @@ function startInteractiveServer(port, onReady) {
1088
1427
  const blocked = evals.filter(e => e.decision === "BLOCK").length;
1089
1428
  const modified = evals.filter(e => e.decision === "MODIFY").length;
1090
1429
  const allowed = evals.filter(e => e.decision === "ALLOW").length;
1430
+ const rewarded = evals.filter(e => e.decision === "REWARD").length;
1431
+ const penalized = evals.filter(e => e.decision === "PENALIZE").length;
1091
1432
  const uniqueActors = [...new Set(evals.map(e => e.actor))];
1092
1433
  const uniqueActions = [...new Set(evals.map(e => e.action))];
1093
1434
  const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
@@ -1101,6 +1442,8 @@ function startInteractiveServer(port, onReady) {
1101
1442
  blocked,
1102
1443
  modified,
1103
1444
  allowed,
1445
+ rewarded,
1446
+ penalized,
1104
1447
  },
1105
1448
  agents: uniqueActors,
1106
1449
  actionTypes: uniqueActions,
@@ -1243,6 +1586,10 @@ function startInteractiveServer(port, onReady) {
1243
1586
  guardCount: customGuards.length,
1244
1587
  evaluations: [],
1245
1588
  };
1589
+ // Reset bridge metrics counters
1590
+ bridgeEvalCount = 0;
1591
+ bridgeBlockCount = 0;
1592
+ bridgeModifyCount = 0;
1246
1593
  jsonResponse(res, 200, {
1247
1594
  status: "reset",
1248
1595
  newSessionId: currentSession.id,
@@ -1303,6 +1650,308 @@ function startInteractiveServer(port, onReady) {
1303
1650
  jsonResponse(res, 200, { adapters });
1304
1651
  return;
1305
1652
  }
1653
+ // ── Agent Behavior States ──
1654
+ // Returns per-agent incentive state (cooldowns, influence, penalties, rewards)
1655
+ if (req.url === "/api/agent-states" && req.method === "GET") {
1656
+ const states = {};
1657
+ for (const [id, state] of agentBehaviorStates) {
1658
+ states[id] = {
1659
+ ...state,
1660
+ isFrozen: state.cooldownRemaining > 0,
1661
+ };
1662
+ }
1663
+ jsonResponse(res, 200, {
1664
+ agents: states,
1665
+ totalTracked: agentBehaviorStates.size,
1666
+ frozen: [...agentBehaviorStates.values()].filter(s => s.cooldownRemaining > 0).length,
1667
+ });
1668
+ return;
1669
+ }
1670
+ // Tick agent cooldowns — called once per simulation round to decrement timers
1671
+ if (req.url === "/api/agent-states/tick" && req.method === "POST") {
1672
+ try {
1673
+ const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
1674
+ if (nv.tickAgentStates) {
1675
+ const updated = nv.tickAgentStates(agentBehaviorStates);
1676
+ // Update our map with ticked values
1677
+ for (const [id, state] of updated) {
1678
+ agentBehaviorStates.set(id, state);
1679
+ }
1680
+ }
1681
+ else {
1682
+ localTickAgentStates();
1683
+ }
1684
+ }
1685
+ catch {
1686
+ localTickAgentStates();
1687
+ }
1688
+ const frozen = [...agentBehaviorStates.values()].filter(s => s.cooldownRemaining > 0).length;
1689
+ jsonResponse(res, 200, { status: "ticked", totalTracked: agentBehaviorStates.size, frozen });
1690
+ return;
1691
+ }
1692
+ // Bridge capabilities — returns what each bridge can/cannot control
1693
+ if (req.url === "/api/bridge-capabilities" && req.method === "GET") {
1694
+ const { SCIENCECLAW_CAPABILITIES } = await Promise.resolve().then(() => __importStar(require("../adapters/scienceclaw")));
1695
+ jsonResponse(res, 200, {
1696
+ bridges: [SCIENCECLAW_CAPABILITIES],
1697
+ });
1698
+ return;
1699
+ }
1700
+ // Bridge connection check — test if a bridge endpoint is reachable
1701
+ if (req.url === "/api/bridge-check" && req.method === "POST") {
1702
+ try {
1703
+ const body = await readBody(req);
1704
+ const { bridgeId, endpoint } = JSON.parse(body);
1705
+ if (bridgeId === "scienceclaw") {
1706
+ // Auto-detect scienceclaw-post on PATH
1707
+ let scienceClawPath = null;
1708
+ let scienceClawVersion = null;
1709
+ try {
1710
+ const whichCmd = process.platform === "win32" ? "where scienceclaw-post" : "which scienceclaw-post";
1711
+ scienceClawPath = (0, child_process_1.execSync)(whichCmd, { encoding: "utf-8", timeout: 3000 }).trim();
1712
+ }
1713
+ catch { /* not found */ }
1714
+ if (!scienceClawPath && endpoint) {
1715
+ // User provided a custom path/endpoint — check if it exists
1716
+ try {
1717
+ if (fs.existsSync(endpoint))
1718
+ scienceClawPath = endpoint;
1719
+ }
1720
+ catch { /* ignore */ }
1721
+ }
1722
+ if (scienceClawPath) {
1723
+ // Try to get version
1724
+ try {
1725
+ scienceClawVersion = (0, child_process_1.execSync)(`"${scienceClawPath}" --version 2>/dev/null || echo "unknown"`, { encoding: "utf-8", timeout: 3000 }).trim();
1726
+ }
1727
+ catch {
1728
+ scienceClawVersion = "detected";
1729
+ }
1730
+ }
1731
+ jsonResponse(res, 200, {
1732
+ connected: true, // governance server is always ready
1733
+ bridgeId,
1734
+ detected: !!scienceClawPath,
1735
+ binaryPath: scienceClawPath,
1736
+ version: scienceClawVersion,
1737
+ endpoint: "this server",
1738
+ governanceReady: true,
1739
+ canLaunch: !!scienceClawPath,
1740
+ note: scienceClawPath
1741
+ ? `ScienceClaw detected at ${scienceClawPath}. Ready to launch governed sessions.`
1742
+ : "ScienceClaw not found on PATH. Governance server ready — ScienceClaw can connect via /api/evaluate, or provide the binary path.",
1743
+ });
1744
+ }
1745
+ else {
1746
+ jsonResponse(res, 400, { error: "Unknown bridge: " + bridgeId });
1747
+ }
1748
+ }
1749
+ catch (err) {
1750
+ jsonResponse(res, 200, { connected: false, error: String(err) });
1751
+ }
1752
+ return;
1753
+ }
1754
+ // ── Bridge Launch — spawn governed ScienceClaw from the visualizer ──
1755
+ if (req.url === "/api/bridge-launch" && req.method === "POST") {
1756
+ try {
1757
+ const body = await readBody(req);
1758
+ const { bridgeId, args: launchArgs, binaryPath } = JSON.parse(body);
1759
+ if (bridgeId !== "scienceclaw") {
1760
+ jsonResponse(res, 400, { error: "Launch only supported for scienceclaw" });
1761
+ return;
1762
+ }
1763
+ // Find binary
1764
+ let bin = binaryPath || "scienceclaw-post";
1765
+ if (!binaryPath) {
1766
+ try {
1767
+ const whichCmd = process.platform === "win32" ? "where scienceclaw-post" : "which scienceclaw-post";
1768
+ bin = (0, child_process_1.execSync)(whichCmd, { encoding: "utf-8", timeout: 3000 }).trim();
1769
+ }
1770
+ catch {
1771
+ jsonResponse(res, 400, { error: "scienceclaw-post not found on PATH. Install ScienceClaw or provide binaryPath." });
1772
+ return;
1773
+ }
1774
+ }
1775
+ // Kill any existing bridge process
1776
+ if (activeBridgeProcess) {
1777
+ try {
1778
+ activeBridgeProcess.kill();
1779
+ }
1780
+ catch { /* ignore */ }
1781
+ activeBridgeProcess = null;
1782
+ }
1783
+ // Build the governed wrapper command
1784
+ // We use the Python wrapper if available, otherwise call directly
1785
+ const wrapperPath = path.join(__dirname, "..", "..", "connectors", "nv_scienceclaw_wrapper.py");
1786
+ const addr = server.address();
1787
+ const nvPort = (addr && typeof addr === "object") ? addr.port : port;
1788
+ const cmdArgs = launchArgs ?? [];
1789
+ let child;
1790
+ let launchMethod;
1791
+ if (fs.existsSync(wrapperPath)) {
1792
+ // Launch through Python governance wrapper
1793
+ child = (0, child_process_1.spawn)("python3", [wrapperPath, ...cmdArgs], {
1794
+ env: {
1795
+ ...process.env,
1796
+ NEUROVERSE_URL: `http://localhost:${nvPort}`,
1797
+ SCIENCECLAW_BIN: bin,
1798
+ },
1799
+ stdio: ["pipe", "pipe", "pipe"],
1800
+ });
1801
+ launchMethod = "python-wrapper";
1802
+ }
1803
+ else {
1804
+ // Launch directly — governance via /api/evaluate endpoint
1805
+ child = (0, child_process_1.spawn)(bin, cmdArgs, {
1806
+ stdio: ["pipe", "pipe", "pipe"],
1807
+ });
1808
+ launchMethod = "direct";
1809
+ }
1810
+ activeBridgeProcess = child;
1811
+ const pid = child.pid;
1812
+ // Stream output to SSE clients
1813
+ child.stdout?.on("data", (chunk) => {
1814
+ const text = chunk.toString();
1815
+ broadcast({ type: "bridge_output", source: "scienceclaw", stream: "stdout", text });
1816
+ // Try parsing JSON lines for live adapter integration
1817
+ for (const line of text.split("\n").filter(Boolean)) {
1818
+ if (line.startsWith("{")) {
1819
+ try {
1820
+ const data = JSON.parse(line);
1821
+ if (data.cycle || data.step) {
1822
+ broadcast({ type: "bridge_round", source: "scienceclaw", data });
1823
+ }
1824
+ }
1825
+ catch { /* not JSON */ }
1826
+ }
1827
+ }
1828
+ });
1829
+ child.stderr?.on("data", (chunk) => {
1830
+ broadcast({ type: "bridge_output", source: "scienceclaw", stream: "stderr", text: chunk.toString() });
1831
+ });
1832
+ child.on("close", (code) => {
1833
+ activeBridgeProcess = null;
1834
+ broadcast({ type: "bridge_exit", source: "scienceclaw", code, pid });
1835
+ });
1836
+ child.on("error", (err) => {
1837
+ activeBridgeProcess = null;
1838
+ broadcast({ type: "bridge_error", source: "scienceclaw", error: err.message });
1839
+ });
1840
+ jsonResponse(res, 200, {
1841
+ launched: true,
1842
+ pid,
1843
+ binary: bin,
1844
+ launchMethod,
1845
+ args: cmdArgs,
1846
+ message: `ScienceClaw launched (PID ${pid}) with ${launchMethod} governance. Output streaming to dashboard.`,
1847
+ });
1848
+ }
1849
+ catch (err) {
1850
+ jsonResponse(res, 500, { error: `Launch failed: ${err instanceof Error ? err.message : String(err)}` });
1851
+ }
1852
+ return;
1853
+ }
1854
+ // ── Bridge Stop — kill running bridge process ──
1855
+ if (req.url === "/api/bridge-stop" && req.method === "POST") {
1856
+ if (activeBridgeProcess) {
1857
+ const pid = activeBridgeProcess.pid;
1858
+ try {
1859
+ activeBridgeProcess.kill();
1860
+ }
1861
+ catch { /* ignore */ }
1862
+ activeBridgeProcess = null;
1863
+ jsonResponse(res, 200, { stopped: true, pid });
1864
+ }
1865
+ else {
1866
+ jsonResponse(res, 200, { stopped: false, message: "No active bridge process" });
1867
+ }
1868
+ return;
1869
+ }
1870
+ // ── Bridge Status — check if a bridge process is running ──
1871
+ if (req.url === "/api/bridge-status" && req.method === "GET") {
1872
+ jsonResponse(res, 200, {
1873
+ running: !!activeBridgeProcess,
1874
+ pid: activeBridgeProcess?.pid ?? null,
1875
+ });
1876
+ return;
1877
+ }
1878
+ // ── AI API Key Management ──
1879
+ // GET: returns current provider config (key masked)
1880
+ // POST: sets provider config for world file compiling and AI-powered features
1881
+ if (req.url === "/api/ai-config" && req.method === "GET") {
1882
+ jsonResponse(res, 200, {
1883
+ provider: userAIConfig.provider,
1884
+ hasKey: userAIConfig.apiKey.length > 0,
1885
+ keyPreview: userAIConfig.apiKey.length > 8
1886
+ ? userAIConfig.apiKey.slice(0, 4) + "..." + userAIConfig.apiKey.slice(-4)
1887
+ : userAIConfig.apiKey.length > 0 ? "****" : "",
1888
+ baseUrl: userAIConfig.baseUrl ?? null,
1889
+ model: userAIConfig.model ?? null,
1890
+ });
1891
+ return;
1892
+ }
1893
+ if (req.url === "/api/ai-config" && req.method === "POST") {
1894
+ try {
1895
+ const body = await readBody(req);
1896
+ const payload = JSON.parse(body);
1897
+ if (payload.provider)
1898
+ userAIConfig.provider = payload.provider;
1899
+ if (payload.apiKey !== undefined)
1900
+ userAIConfig.apiKey = payload.apiKey;
1901
+ if (payload.baseUrl !== undefined)
1902
+ userAIConfig.baseUrl = payload.baseUrl || undefined;
1903
+ if (payload.model !== undefined)
1904
+ userAIConfig.model = payload.model || undefined;
1905
+ jsonResponse(res, 200, {
1906
+ status: "updated",
1907
+ provider: userAIConfig.provider,
1908
+ hasKey: userAIConfig.apiKey.length > 0,
1909
+ });
1910
+ }
1911
+ catch {
1912
+ jsonResponse(res, 400, { error: "Invalid request body" });
1913
+ }
1914
+ return;
1915
+ }
1916
+ // Generate governance rules from thesis description
1917
+ if (req.url === "/api/generate-rules" && req.method === "POST") {
1918
+ try {
1919
+ const body = await readBody(req);
1920
+ const { thesis } = JSON.parse(body);
1921
+ if (!thesis || thesis.trim().length === 0) {
1922
+ jsonResponse(res, 400, { error: "thesis is required" });
1923
+ return;
1924
+ }
1925
+ // Use existing policy engine to generate a world from the thesis
1926
+ const { parseRulesFromText, policyToWorld } = await Promise.resolve().then(() => __importStar(require("./policyEngine")));
1927
+ const parsed = parseRulesFromText(thesis);
1928
+ const world = policyToWorld(parsed);
1929
+ // Also set custom guards so they're active for governance
1930
+ customGuards.length = 0;
1931
+ parsed.rules.forEach((r, i) => {
1932
+ customGuards.push({
1933
+ id: r.id || "user-rule-" + (i + 1),
1934
+ label: r.description,
1935
+ description: r.description,
1936
+ category: "user",
1937
+ enforcement: r.enforcement || "block",
1938
+ immutable: false,
1939
+ intent_patterns: r.intent_patterns || [],
1940
+ default_enabled: true,
1941
+ });
1942
+ });
1943
+ jsonResponse(res, 200, {
1944
+ status: "generated",
1945
+ parsed: { total: parsed.rules.length, rules: parsed.rules },
1946
+ world,
1947
+ guardsApplied: customGuards.length,
1948
+ });
1949
+ }
1950
+ catch (err) {
1951
+ jsonResponse(res, 400, { error: "Failed to generate rules: " + String(err) });
1952
+ }
1953
+ return;
1954
+ }
1306
1955
  // Run simulation via live adapter (external process)
1307
1956
  if (req.url === "/api/run-live" && req.method === "POST") {
1308
1957
  if (isRunning) {
@@ -1321,7 +1970,7 @@ function startInteractiveServer(port, onReady) {
1321
1970
  jsonResponse(res, 200, { status: "started", adapter: payload.adapterId });
1322
1971
  // Resolve world for governance evaluation
1323
1972
  const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
1324
- const resolved = resolveWorld(payload.worldId ?? "trading");
1973
+ const resolved = resolveWorld(payload.worldId ?? "social_simulation");
1325
1974
  const world = { ...resolved.world };
1326
1975
  if (payload.stateOverrides) {
1327
1976
  const updatedVars = world.state_variables.map(sv => {
@@ -1356,7 +2005,7 @@ function startInteractiveServer(port, onReady) {
1356
2005
  // Listen for rounds from the adapter
1357
2006
  adapter.on("round", (liveRound) => {
1358
2007
  // Build lookup of original verdicts from adapter's adaptation data.
1359
- // When an external bridge (e.g. MiroFish) already applied governance,
2008
+ // When an external bridge already applied governance,
1360
2009
  // the verdict lives in adaptation.deltas — use it instead of re-evaluating
1361
2010
  // (re-evaluating the MODIFIED action would return ALLOW, hiding the BLOCK).
1362
2011
  const adaptationByAgent = new Map();
@@ -1636,42 +2285,80 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1636
2285
  .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; }
1637
2286
 
1638
2287
  /* RIGHT PANEL — Simulation viewer */
1639
- .viewer { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
1640
- .viewer-top { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); }
1641
- .viewer-mid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); overflow: hidden; }
1642
- .viewer-bottom { background: var(--bg-primary); border-top: 1px solid var(--border); padding: 12px 16px; max-height: 180px; overflow-y: auto; }
1643
-
1644
- .vpanel { background: var(--bg-primary); padding: 14px; overflow-y: auto; }
1645
- .vpanel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
1646
-
1647
- /* Metrics */
1648
- .metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
1649
- .metric-box { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 10px; text-align: center; }
1650
- .metric-box .value { font-size: 20px; font-weight: 700; color: var(--text-primary); }
1651
- .metric-box .label { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
1652
- .metric-box.good .value { color: var(--green); }
1653
- .metric-box.bad .value { color: var(--red); }
1654
- .metric-box.warn .value { color: var(--yellow); }
1655
-
1656
- /* Agent bars */
1657
- .agent-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; }
1658
- .agent-name { width: 130px; color: var(--text-secondary); flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1659
- .impact-bar-bg { flex: 1; height: 14px; background: var(--bg-surface); border-radius: 3px; position: relative; overflow: hidden; }
1660
- .impact-bar { height: 100%; border-radius: 3px; transition: width 0.4s ease; position: absolute; top: 0; }
1661
- .impact-bar.positive { background: var(--green); right: 50%; }
1662
- .impact-bar.negative { background: var(--red); left: 50%; }
1663
- .impact-val { width: 44px; text-align: right; color: var(--text-secondary); font-size: 10px; }
2288
+ .viewer { display: grid; grid-template-rows: auto auto auto auto auto; overflow-y: auto; height: 100%; gap: 0; }
2289
+
2290
+ /* Outcome panel */
2291
+ .outcome-panel { padding: 20px 24px 16px; border-bottom: 1px solid var(--border); }
2292
+ .outcome-statement { font-size: 16px; font-weight: 600; color: var(--text-primary); line-height: 1.4; margin-bottom: 12px; }
2293
+ .outcome-empty { font-size: 13px; color: var(--text-faint); font-style: italic; }
2294
+ .confidence-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 8px; }
2295
+ .confidence-card { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 8px 10px; }
2296
+ .confidence-card .cc-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
2297
+ .confidence-card .cc-value { font-size: 13px; font-weight: 600; }
2298
+ .confidence-card .cc-value.good { color: var(--green); }
2299
+ .confidence-card .cc-value.warn { color: var(--yellow); }
2300
+ .confidence-card .cc-value.bad { color: var(--red); }
2301
+ .outcome-context { font-size: 10px; color: var(--text-faint); margin-top: 6px; }
2302
+
2303
+ /* Behavior panel */
2304
+ .behavior-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
2305
+ .behavior-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
2306
+ .behavior-shifts { margin-bottom: 10px; }
2307
+ .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; }
2308
+ .behavior-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
2309
+
2310
+ .activity-toggle { font-size: 10px; color: var(--text-muted); cursor: pointer; padding: 6px 0; margin-top: 4px; }
2311
+ .activity-toggle:hover { color: var(--text-secondary); }
2312
+ .activity-timeline { display: none; max-height: 180px; overflow-y: auto; margin-top: 6px; }
2313
+ .activity-timeline.open { display: block; }
2314
+ .activity-item { font-size: 11px; color: var(--text-secondary); padding: 3px 0; border-bottom: 1px solid var(--border); }
2315
+ .activity-item:last-child { border-bottom: none; }
2316
+ .activity-agent { color: var(--blue); font-weight: 500; }
2317
+
2318
+ /* Why panel */
2319
+ .why-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
2320
+ .why-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
2321
+ .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; }
2322
+ .why-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
2323
+
2324
+ /* Export bar */
2325
+ .export-bar { display: flex; gap: 8px; padding: 12px 24px; border-bottom: 1px solid var(--border); background: var(--bg-secondary); }
2326
+ .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; }
2327
+ .export-btn:hover { background: var(--bg-elevated); color: var(--text-primary); }
2328
+ .audit-btn { margin-left: auto; color: var(--text-faint); }
2329
+
2330
+ /* Audit overlay */
2331
+ .audit-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; background: rgba(0,0,0,0.7); }
2332
+ .audit-overlay.open { display: flex; align-items: center; justify-content: center; }
2333
+ .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; }
2334
+ .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; }
2335
+ .audit-header h2 { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; }
2336
+ .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; }
2337
+ .audit-close:hover { color: var(--text-primary); }
2338
+ .audit-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
2339
+ .audit-section:last-child { border-bottom: none; }
2340
+ .audit-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
2341
+ .audit-section .rule-card { margin-bottom: 6px; }
2342
+
2343
+ /* Print styles for PDF export */
2344
+ @media print {
2345
+ body { background: #fff !important; color: #111 !important; }
2346
+ .controls, .export-bar, .audit-overlay, .activity-toggle { display: none !important; }
2347
+ .viewer { overflow: visible !important; height: auto !important; }
2348
+ .outcome-panel, .behavior-panel, .why-panel { border: 1px solid #ddd !important; margin-bottom: 8px; border-radius: 6px; }
2349
+ .confidence-card { border: 1px solid #ddd !important; }
2350
+ * { color: #111 !important; background: #fff !important; border-color: #ddd !important; }
2351
+ .cc-value.good { color: #16a34a !important; }
2352
+ .cc-value.warn { color: #ca8a04 !important; }
2353
+ .cc-value.bad { color: #dc2626 !important; }
2354
+ }
1664
2355
  .center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
1665
2356
  .verdict { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-left: 4px; }
1666
2357
  .verdict.ALLOW { background: var(--green-bg); color: var(--green); }
1667
2358
  .verdict.BLOCK { background: var(--red-bg); color: var(--red); }
1668
2359
  .verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
1669
2360
 
1670
- /* Chart */
1671
- .chart-container { position: relative; height: 100%; min-height: 150px; }
1672
- canvas { width: 100% !important; height: 100% !important; }
1673
-
1674
- /* Simulation Trace */
2361
+ /* Simulation Trace (audit only) */
1675
2362
  .trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
1676
2363
  .trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
1677
2364
  .trace-round-header:hover { background: var(--bg-elevated); }
@@ -1781,6 +2468,239 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1781
2468
  .integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
1782
2469
  .integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
1783
2470
 
2471
+ /* Language tabs for integration code */
2472
+ .lang-tabs { display: flex; gap: 2px; margin-bottom: 6px; }
2473
+ .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; }
2474
+ .lang-tab:hover { color: var(--text-secondary); }
2475
+ .lang-tab.active { color: var(--accent); background: var(--bg-surface); border-color: var(--bg-elevated); }
2476
+ .lang-code-panel { display: none; }
2477
+ .lang-code-panel.active { display: block; }
2478
+ .integrate-hint { font-size: 10px; color: var(--text-muted); margin-top: 6px; line-height: 1.5; }
2479
+ .integrate-hint strong { color: var(--text-secondary); }
2480
+ .integrate-works { font-size: 10px; color: var(--text-faint); margin-top: 8px; line-height: 1.6; }
2481
+ .integrate-works strong { color: var(--text-muted); }
2482
+
2483
+ /* Framework selector tabs */
2484
+ .fw-tabs { display: flex; gap: 4px; margin-bottom: 10px; }
2485
+ .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; }
2486
+ .fw-tab:hover { border-color: var(--text-muted); color: var(--text-secondary); }
2487
+ .fw-tab.active { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
2488
+ .fw-tab .fw-sub { display: block; font-size: 8px; font-weight: 400; color: var(--text-faint); margin-top: 2px; }
2489
+ .fw-tab.active .fw-sub { color: var(--accent); opacity: 0.7; }
2490
+ .fw-panel { display: none; }
2491
+ .fw-panel.active { display: block; }
2492
+
2493
+ /* Architecture callout */
2494
+ .arch-callout { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; }
2495
+ .arch-callout .arch-title { font-size: 10px; font-weight: 700; color: var(--text-secondary); margin-bottom: 4px; }
2496
+ .arch-flow { font-size: 10px; color: var(--text-muted); font-family: monospace; line-height: 1.6; }
2497
+ .arch-flow .arch-yes { color: var(--green); }
2498
+ .arch-flow .arch-no { color: var(--red); }
2499
+ .arch-warn { font-size: 9px; color: var(--red); margin-top: 6px; padding: 4px 8px; background: #2d0606; border-radius: 4px; line-height: 1.5; }
2500
+
2501
+ /* Responsive / Adaptive Panels */
2502
+ @media (max-width: 1200px) {
2503
+ .layout { grid-template-columns: 300px 1fr; }
2504
+ .controls { padding: 12px; }
2505
+ }
2506
+ @media (max-width: 960px) {
2507
+ .layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; height: auto; min-height: 100vh; }
2508
+ .controls { border-right: none; border-bottom: 1px solid var(--border); max-height: 50vh; overflow-y: auto; }
2509
+ .viewer { min-height: 60vh; }
2510
+ .viewer-top { grid-template-columns: 1fr; }
2511
+ .viewer-mid { grid-template-columns: 1fr; }
2512
+ .metric-grid { grid-template-columns: repeat(2, 1fr); }
2513
+ }
2514
+ @media (max-width: 640px) {
2515
+ .header h1 { font-size: 14px; }
2516
+ .header .sub { font-size: 10px; }
2517
+ .controls { padding: 10px; }
2518
+ .ctrl-section h3 { font-size: 10px; }
2519
+ .confidence-grid { grid-template-columns: 1fr; }
2520
+ .agent-name { width: 80px; }
2521
+ .integrate-code { font-size: 9px; padding: 6px; }
2522
+ .scenario-btn { padding: 6px; }
2523
+ }
2524
+
2525
+ /* Bridge Selector */
2526
+ .bridge-selector { display: flex; gap: 6px; margin-bottom: 8px; }
2527
+ .bridge-btn { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 12px 8px; background: var(--bg-surface); border: 2px solid var(--border); border-radius: 6px; cursor: pointer; transition: all 0.2s; font-family: inherit; color: var(--text-primary); }
2528
+ .bridge-btn:hover { border-color: var(--text-muted); }
2529
+ .bridge-btn.active { border-color: var(--accent); background: var(--accent-bg); }
2530
+ .bridge-name { font-size: 12px; font-weight: 700; }
2531
+ .bridge-sub { font-size: 9px; color: var(--text-muted); margin-top: 2px; }
2532
+ .bridge-btn.active .bridge-name { color: var(--accent); }
2533
+ .bridge-status { font-size: 11px; margin-top: 4px; }
2534
+ .bridge-setup { margin-top: 8px; animation: fadeIn 0.2s ease; }
2535
+ .bridge-instructions { font-size: 11px; color: var(--text-secondary); line-height: 1.6; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 4px; padding: 10px; margin-bottom: 8px; white-space: pre-wrap; }
2536
+ .bridge-connect-row { display: flex; gap: 6px; }
2537
+ .bridge-endpoint-input { flex: 1; 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; }
2538
+ .bridge-endpoint-input:focus { border-color: var(--accent); outline: none; }
2539
+ .btn-connect { background: var(--accent); color: #fff; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-family: inherit; font-size: 12px; font-weight: 600; }
2540
+ .btn-connect:hover { filter: brightness(1.1); }
2541
+ .bridge-info-row { font-size: 11px; color: var(--text-secondary); padding: 4px 0; display: flex; align-items: center; gap: 6px; }
2542
+ .bridge-info-icon { font-size: 8px; }
2543
+
2544
+ /* World File Toggle */
2545
+ .worldfile-toggle { font-size: 11px; color: var(--text-muted); cursor: pointer; padding: 4px 0; transition: color 0.2s; }
2546
+ .worldfile-toggle:hover { color: var(--text-secondary); }
2547
+ .wf-arrow { font-size: 9px; margin-right: 4px; display: inline-block; transition: transform 0.2s; }
2548
+ .worldfile-hint { font-size: 10px; color: var(--text-faint); margin-top: 8px; line-height: 1.5; }
2549
+ .worldfile-hint strong { color: var(--accent); }
2550
+ .worldfile-hint code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
2551
+
2552
+ /* Governance Suggestions */
2553
+ .governance-suggestions { }
2554
+ .suggestion-item { background: var(--bg-surface); border-radius: 4px; padding: 8px 10px; margin-bottom: 6px; animation: fadeIn 0.3s ease; }
2555
+ .suggestion-text { font-size: 11px; color: var(--text-secondary); line-height: 1.5; }
2556
+
2557
+ /* ============================================ */
2558
+ /* OBSERVATION DECK */
2559
+ /* ============================================ */
2560
+
2561
+ .deck-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px 8px; }
2562
+ .deck-title { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; letter-spacing: 0.5px; }
2563
+ .deck-status { font-size: 10px; color: var(--text-faint); padding: 3px 8px; border: 1px solid var(--border); border-radius: 10px; }
2564
+ .deck-status.live { color: var(--green); border-color: var(--green); }
2565
+ .deck-status.complete { color: var(--accent); border-color: var(--accent); }
2566
+
2567
+ .deck-section { padding: 0 20px 16px; }
2568
+ .deck-label { font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
2569
+
2570
+ /* Choice Landscape — bubble visualization */
2571
+ .choice-landscape { min-height: 180px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: center; position: relative; transition: all 0.3s ease; }
2572
+ .choice-empty { text-align: center; color: var(--text-faint); font-size: 11px; line-height: 1.6; }
2573
+ .choice-empty-icon { font-size: 32px; opacity: 0.3; margin-bottom: 8px; }
2574
+
2575
+ /* Choice Bubble */
2576
+ .choice-bubble { display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.6s cubic-bezier(0.25, 0.8, 0.25, 1); position: relative; cursor: default; }
2577
+ .choice-bubble .cb-count { font-weight: 800; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.4); }
2578
+ .choice-bubble .cb-label { font-size: 8px; color: rgba(255,255,255,0.85); text-align: center; max-width: 90%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; letter-spacing: 0.3px; }
2579
+ .choice-bubble .cb-pct { font-size: 8px; color: rgba(255,255,255,0.6); margin-top: 1px; }
2580
+
2581
+ /* Decision Flow — intended → rule → actual */
2582
+ .decision-flow { margin-top: 12px; }
2583
+ .flow-row { display: grid; grid-template-columns: 1fr 40px 1fr; gap: 0; align-items: center; margin-bottom: 6px; min-height: 32px; }
2584
+ .flow-intended { display: flex; align-items: center; gap: 6px; justify-content: flex-end; }
2585
+ .flow-actual { display: flex; align-items: center; gap: 6px; }
2586
+ .flow-bar { height: 24px; border-radius: 4px; display: flex; align-items: center; padding: 0 8px; font-size: 10px; font-weight: 600; min-width: 20px; transition: width 0.6s cubic-bezier(0.25, 0.8, 0.25, 1); }
2587
+ .flow-bar.intended { justify-content: flex-end; background: var(--bg-elevated); color: var(--text-secondary); border-radius: 4px 0 0 4px; }
2588
+ .flow-bar.actual-allow { background: #052e16; color: #4ade80; border-radius: 0 4px 4px 0; }
2589
+ .flow-bar.actual-block { background: #2d0606; color: #f87171; border-radius: 0 4px 4px 0; }
2590
+ .flow-bar.actual-modify { background: #2d2006; color: #fbbf24; border-radius: 0 4px 4px 0; }
2591
+ .flow-arrow { text-align: center; font-size: 12px; color: var(--text-faint); }
2592
+ .flow-arrow.blocked { color: var(--red); }
2593
+ .flow-arrow.modified { color: var(--yellow); }
2594
+ .flow-arrow.allowed { color: var(--green); }
2595
+ .flow-label { font-size: 9px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2596
+ .flow-count { font-size: 10px; font-weight: 700; min-width: 16px; }
2597
+ .flow-legend { display: flex; gap: 12px; margin-top: 8px; justify-content: center; }
2598
+ .flow-legend-item { font-size: 9px; color: var(--text-faint); display: flex; align-items: center; gap: 4px; }
2599
+ .flow-legend-dot { width: 6px; height: 6px; border-radius: 50%; }
2600
+
2601
+ /* Choice Round Label */
2602
+ .choice-round-label { font-size: 10px; color: var(--text-faint); text-align: center; margin-top: 6px; }
2603
+
2604
+ /* Choice Stream */
2605
+ .choice-stream { max-height: 160px; overflow-y: auto; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px; }
2606
+ .stream-empty { font-size: 11px; color: var(--text-faint); text-align: center; padding: 16px 0; }
2607
+ .stream-item { font-size: 10px; color: var(--text-secondary); padding: 3px 0; border-bottom: 1px solid var(--border); display: flex; gap: 8px; align-items: baseline; animation: streamIn 0.3s ease; }
2608
+ .stream-item:last-child { border-bottom: none; }
2609
+ @keyframes streamIn { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: translateX(0); } }
2610
+ .stream-agent { color: var(--accent); font-weight: 600; min-width: 80px; }
2611
+ .stream-action { flex: 1; }
2612
+ .stream-round { color: var(--text-faint); font-size: 9px; }
2613
+ .stream-count { font-size: 9px; color: var(--text-faint); }
2614
+
2615
+ /* Behavioral Drift — post-run comparison */
2616
+ .drift-verdict { font-size: 14px; font-weight: 700; color: var(--text-primary); line-height: 1.5; margin-bottom: 12px; }
2617
+ .drift-comparison { display: grid; gap: 8px; }
2618
+ .drift-row { display: grid; grid-template-columns: 120px 1fr 40px 1fr; gap: 8px; align-items: center; font-size: 11px; }
2619
+ .drift-action { color: var(--text-secondary); font-weight: 600; text-align: right; }
2620
+ .drift-bar-container { height: 20px; background: var(--bg-elevated); border-radius: 3px; overflow: hidden; position: relative; }
2621
+ .drift-bar { height: 100%; border-radius: 3px; transition: width 0.8s cubic-bezier(0.25, 0.8, 0.25, 1); display: flex; align-items: center; padding-left: 6px; font-size: 9px; font-weight: 700; color: rgba(255,255,255,0.9); }
2622
+ .drift-bar.early { background: linear-gradient(90deg, var(--text-muted), var(--text-faint)); }
2623
+ .drift-bar.late { background: linear-gradient(90deg, var(--accent), #6366f1); }
2624
+ .drift-arrow-col { text-align: center; font-size: 14px; color: var(--text-faint); }
2625
+ .drift-delta { font-size: 10px; font-weight: 700; text-align: center; }
2626
+ .drift-delta.up { color: var(--green); }
2627
+ .drift-delta.down { color: var(--red); }
2628
+ .drift-delta.flat { color: var(--text-faint); }
2629
+
2630
+ .drift-header-row { display: grid; grid-template-columns: 120px 1fr 40px 1fr; gap: 8px; font-size: 9px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
2631
+ .drift-header-row span:first-child { text-align: right; }
2632
+ .drift-header-row span:nth-child(3) { text-align: center; }
2633
+
2634
+ /* What-If section */
2635
+ .whatif-content { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
2636
+ .whatif-stat { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
2637
+ .whatif-num { font-size: 28px; font-weight: 800; }
2638
+ .whatif-num.bad { color: var(--red); }
2639
+ .whatif-num.good { color: var(--green); }
2640
+ .whatif-label { font-size: 12px; color: var(--text-secondary); }
2641
+ .whatif-compare { font-size: 11px; color: var(--text-muted); margin-top: 8px; line-height: 1.5; }
2642
+
2643
+ /* Start Here button */
2644
+ .btn-start-here { width: 100%; padding: 8px; margin-bottom: 12px; background: transparent; color: var(--accent); border: 1px dashed var(--accent); border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.2s; letter-spacing: 0.5px; }
2645
+ .btn-start-here:hover { background: var(--accent-bg); border-style: solid; }
2646
+
2647
+ /* AI API Key Section — always visible */
2648
+ .ai-key-section { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 16px; }
2649
+ .ai-key-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
2650
+ .ai-key-label { font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: 600; }
2651
+ .ai-key-status { font-size: 10px; padding: 2px 8px; border-radius: 10px; }
2652
+ .ai-key-status.connected { background: rgba(74, 222, 128, 0.15); color: #4ade80; }
2653
+ .ai-key-status.missing { background: rgba(248, 113, 113, 0.15); color: #f87171; }
2654
+ .ai-key-provider-row { display: flex; gap: 4px; margin-bottom: 8px; }
2655
+ .ai-key-provider-btn { flex: 1; padding: 5px 4px; background: var(--bg-secondary); color: var(--text-muted); border: 1px solid var(--border-subtle); border-radius: 4px; font-size: 10px; font-family: inherit; cursor: pointer; transition: all 0.2s; text-align: center; }
2656
+ .ai-key-provider-btn:hover { border-color: var(--text-muted); }
2657
+ .ai-key-provider-btn.active { background: var(--accent-bg); color: var(--accent); border-color: var(--accent); }
2658
+ .ai-key-input { width: 100%; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 11px; }
2659
+ .ai-key-input:focus { border-color: var(--accent); outline: none; }
2660
+ .ai-key-input::placeholder { color: var(--text-faint); }
2661
+ .ai-key-extra { margin-top: 6px; }
2662
+ .ai-key-extra input { width: 100%; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 5px 8px; border-radius: 4px; font-family: inherit; font-size: 10px; margin-bottom: 4px; }
2663
+ .ai-key-extra input:focus { border-color: var(--accent); outline: none; }
2664
+ .ai-key-save-row { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
2665
+ .btn-save-key { padding: 5px 12px; background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent); border-radius: 4px; font-size: 10px; font-family: inherit; cursor: pointer; transition: all 0.2s; }
2666
+ .btn-save-key:hover { filter: brightness(1.2); }
2667
+ .ai-key-hint { font-size: 9px; color: var(--text-faint); line-height: 1.4; }
2668
+
2669
+ /* Incentive indicators (reward/penalize) */
2670
+ .incentive-badge { display: inline-flex; align-items: center; gap: 3px; font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 10px; }
2671
+ .incentive-badge.reward { background: rgba(74, 222, 128, 0.15); color: #4ade80; }
2672
+ .incentive-badge.penalize { background: rgba(251, 146, 60, 0.15); color: #fb923c; }
2673
+ .flow-arrow.rewarded { color: #4ade80; font-size: 16px; }
2674
+ .flow-arrow.penalized { color: #fb923c; font-size: 16px; }
2675
+ .actual-reward { background: rgba(74, 222, 128, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); }
2676
+ .actual-penalize { background: rgba(251, 146, 60, 0.2); border: 1px solid rgba(251, 146, 60, 0.3); }
2677
+
2678
+ /* Onboarding Overlay */
2679
+ .onboard-overlay { position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.7); backdrop-filter: blur(6px); animation: fadeIn 0.3s ease; }
2680
+ .onboard-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 12px; max-width: 520px; width: 90vw; max-height: 85vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
2681
+ .onboard-header { padding: 24px 24px 0; }
2682
+ .onboard-header h2 { font-size: 18px; color: var(--text-primary); font-weight: 700; margin: 0; }
2683
+ .onboard-header .onboard-tagline { font-size: 13px; color: var(--text-secondary); margin-top: 6px; line-height: 1.5; }
2684
+ .onboard-body { padding: 16px 24px 24px; }
2685
+ .onboard-step { display: flex; gap: 12px; margin-bottom: 16px; }
2686
+ .onboard-step-num { min-width: 24px; height: 24px; background: var(--accent-bg); color: var(--accent); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; flex-shrink: 0; margin-top: 2px; }
2687
+ .onboard-step-content h4 { font-size: 13px; color: var(--text-primary); font-weight: 700; margin: 0 0 4px; }
2688
+ .onboard-step-content p { font-size: 11px; color: var(--text-muted); margin: 0; line-height: 1.5; }
2689
+ .onboard-example { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 12px; margin: 12px 0; }
2690
+ .onboard-example-label { font-size: 9px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
2691
+ .onboard-example-text { font-size: 12px; color: var(--accent); font-style: italic; line-height: 1.5; }
2692
+ .onboard-example-result { font-size: 10px; color: var(--text-muted); margin-top: 8px; line-height: 1.5; }
2693
+ .onboard-example-result strong { color: var(--text-secondary); }
2694
+ .onboard-section { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); }
2695
+ .onboard-section h4 { font-size: 11px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 1px; margin: 0 0 8px; }
2696
+ .onboard-capability { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
2697
+ .onboard-capability .cap-icon { color: var(--green); flex-shrink: 0; margin-top: 1px; }
2698
+ .onboard-footer { padding: 0 24px 20px; display: flex; gap: 8px; align-items: center; }
2699
+ .btn-onboard-start { flex: 1; padding: 12px; background: var(--accent); color: #fff; border: none; border-radius: 6px; font-size: 13px; font-weight: 700; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
2700
+ .btn-onboard-start:hover { filter: brightness(1.1); }
2701
+ .onboard-dismiss { font-size: 10px; color: var(--text-faint); cursor: pointer; white-space: nowrap; }
2702
+ .onboard-dismiss:hover { color: var(--text-muted); }
2703
+
1784
2704
  /* Rule editor */
1785
2705
  .rule-editor { margin-top: 8px; }
1786
2706
  .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; }
@@ -1870,7 +2790,7 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1870
2790
  <body>
1871
2791
  <div class="header">
1872
2792
  <div style="display:flex;align-items:center">
1873
- <h1>NV-SIM</h1>
2793
+ <h1>NeuroVerse</h1>
1874
2794
  <span class="sub">Governance Runtime</span>
1875
2795
  </div>
1876
2796
  <div class="header-right">
@@ -1882,116 +2802,58 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1882
2802
  <div class="layout">
1883
2803
  <!-- LEFT: CONTROLS -->
1884
2804
  <div class="controls" id="controls-panel">
1885
- <!-- World Action Bar -->
1886
- <div class="world-action-bar">
1887
- <button class="btn btn-world-action" id="new-world-btn" title="Clear everything and start fresh">+ New World</button>
1888
- <button class="btn btn-world-action" id="load-file-btn" title="Load a .json world file">Load World File</button>
1889
- <button class="btn btn-world-action" id="clear-rules-btn" title="Clear custom rules only">Clear Rules</button>
1890
- <button class="btn btn-world-action btn-export" id="export-world-btn" title="Export current world as JSON">Save as World File</button>
1891
- </div>
1892
2805
 
1893
- <!-- World Source selector -->
1894
- <div class="ctrl-section">
1895
- <h3>World Source</h3>
1896
- <div class="world-source-tabs">
1897
- <label class="ws-tab active" data-source="preset">
1898
- <input type="radio" name="world-source" value="preset" checked>
1899
- <span class="ws-label">Preset</span>
1900
- <span class="ws-hint">Demo scenarios</span>
1901
- </label>
1902
- <label class="ws-tab" data-source="custom">
1903
- <input type="radio" name="world-source" value="custom">
1904
- <span class="ws-label">Custom Rules</span>
1905
- <span class="ws-hint">Define your world</span>
1906
- </label>
1907
- <label class="ws-tab" data-source="upload">
1908
- <input type="radio" name="world-source" value="upload">
1909
- <span class="ws-label">World File</span>
1910
- <span class="ws-hint">JSON / .nv-world</span>
1911
- </label>
1912
- </div>
1913
- </div>
2806
+ <!-- Start Here -->
2807
+ <button class="btn-start-here" id="start-here-btn">Start Here</button>
1914
2808
 
1915
- <!-- SOURCE: Preset -->
1916
- <div class="world-source-panel" id="source-preset">
1917
- <div class="ctrl-section">
1918
- <h3>World</h3>
1919
- <div class="ctrl-row">
1920
- <select id="world-select"></select>
1921
- </div>
1922
- <div id="world-thesis" class="world-thesis"></div>
2809
+ <!-- AI API Key — always visible -->
2810
+ <div class="ai-key-section" id="ai-key-section">
2811
+ <div class="ai-key-header">
2812
+ <span class="ai-key-label">AI Runtime Key</span>
2813
+ <span class="ai-key-status missing" id="ai-key-status">No key</span>
1923
2814
  </div>
1924
-
1925
- <!-- State variables (dynamic sliders) -->
1926
- <div class="ctrl-section" id="state-vars-section" style="display:none">
1927
- <h3>World Rules</h3>
1928
- <div id="state-vars"></div>
2815
+ <div class="ai-key-provider-row">
2816
+ <button class="ai-key-provider-btn active" id="ai-prov-anthropic" data-provider="anthropic">Anthropic</button>
2817
+ <button class="ai-key-provider-btn" id="ai-prov-openai" data-provider="openai">OpenAI</button>
2818
+ <button class="ai-key-provider-btn" id="ai-prov-custom" data-provider="custom">Custom</button>
1929
2819
  </div>
1930
-
1931
- <!-- Scenario presets -->
1932
- <div class="ctrl-section">
1933
- <h3>Scenarios</h3>
1934
- <div id="scenario-list"></div>
2820
+ <input type="password" class="ai-key-input" id="ai-key-input" placeholder="sk-... or api key" autocomplete="off">
2821
+ <div class="ai-key-extra" id="ai-key-extra" style="display:none">
2822
+ <input type="text" id="ai-base-url" placeholder="Base URL (e.g. http://localhost:11434/v1)">
2823
+ <input type="text" id="ai-model" placeholder="Model name (e.g. gpt-4o, llama3)">
1935
2824
  </div>
1936
- </div>
1937
-
1938
- <!-- SOURCE: Custom Rules (Define Your World) -->
1939
- <div class="world-source-panel" id="source-custom" style="display:none">
1940
- <div class="ctrl-section">
1941
- <h3>Define Your World</h3>
1942
- <div class="custom-world-header">
1943
- <input type="text" class="world-name-input" id="custom-world-name" placeholder="World name (e.g. Marketing Governance)">
1944
- <textarea class="world-thesis-input" id="custom-world-thesis" placeholder="What is this world about? (thesis)"></textarea>
1945
- </div>
1946
- <div class="rule-editor">
1947
- <div class="rule-editor-label">Type your governance rules:</div>
1948
- <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>
1949
- <button class="btn btn-generate-world" id="parse-rules-btn">Generate World</button>
1950
- <div id="parsed-rules" class="parsed-rules"></div>
1951
- <div id="rule-status" class="rule-status"></div>
1952
- <div class="rule-examples">
1953
- Rule patterns:<br>
1954
- <code>Block [action]</code> — hard suppression<br>
1955
- <code>Limit [X] to [N]</code> — cap extremes<br>
1956
- <code>Require [X] for [Y]</code> — structural constraint<br>
1957
- <code>Pause [X] for review</code> — human-in-the-loop<br>
1958
- <code>Allow [X]</code> — explicit permission<br>
1959
- <code>Monitor [X]</code> — circuit breaker gate
1960
- </div>
1961
- </div>
2825
+ <div class="ai-key-save-row">
2826
+ <button class="btn-save-key" id="ai-key-save-btn">Save Key</button>
2827
+ <span class="ai-key-hint">Required for world file compiling &amp; AI features</span>
1962
2828
  </div>
2829
+ </div>
1963
2830
 
1964
- <!-- Base world (optional) -->
1965
- <div class="ctrl-section">
1966
- <h3>Base World (Optional)</h3>
1967
- <div class="ctrl-row">
1968
- <select id="custom-base-world">
1969
- <option value="">None — start from scratch</option>
1970
- </select>
1971
- <div style="font-size:10px;color:var(--text-faint);margin-top:4px">Layer your rules on top of a preset world</div>
1972
- </div>
1973
- </div>
2831
+ <!-- STEP 1: What are you trying to control? -->
2832
+ <div class="ctrl-section">
2833
+ <h3>What are you trying to control?</h3>
2834
+ <textarea class="rule-input" id="thesis-input" placeholder="Describe what you want to govern in plain English.&#10;&#10;Examples:&#10;- Ensure no single agent dominates more than 15% of activity&#10;- Prevent panic-driven cascades in volatile markets&#10;- Block unverified research from being published&#10;- Require approval for transactions over $10k"></textarea>
2835
+ <button class="btn btn-generate-world" id="generate-rules-btn" style="margin-top:8px">Generate Governance Rules</button>
2836
+ <div id="rule-generation-status" class="rule-status"></div>
2837
+ <div id="generated-rules-preview" class="parsed-rules"></div>
1974
2838
  </div>
1975
2839
 
1976
- <!-- SOURCE: Upload World File -->
1977
- <div class="world-source-panel" id="source-upload" style="display:none">
1978
- <div class="ctrl-section">
1979
- <h3>Load World File</h3>
2840
+ <!-- OR: Load World File -->
2841
+ <div class="ctrl-section">
2842
+ <div class="worldfile-toggle" id="worldfile-toggle">
2843
+ <span class="wf-arrow">&#x25B6;</span> Or load a world file
2844
+ </div>
2845
+ <div id="worldfile-panel" style="display:none">
1980
2846
  <div class="upload-zone" id="upload-zone">
1981
- <div class="upload-icon">&#x1F4C4;</div>
1982
2847
  <div class="upload-label">Drop a .json or .nv-world file here</div>
1983
2848
  <div class="upload-or">or</div>
1984
2849
  <button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
1985
2850
  <input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
1986
2851
  </div>
1987
- <div class="upload-paste-section">
1988
- <div class="rule-editor-label">Or paste world JSON:</div>
1989
- <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>
2852
+ <div class="upload-paste-section" style="margin-top:8px">
2853
+ <textarea class="rule-input" id="world-json-input" placeholder="Or paste world JSON here..." style="min-height:60px"></textarea>
1990
2854
  </div>
1991
- <button class="btn btn-load-world" id="load-world-btn">Load into Runtime</button>
2855
+ <button class="btn btn-load-world" id="load-world-btn" style="margin-top:6px">Load into Runtime</button>
1992
2856
  <div id="upload-status" class="rule-status"></div>
1993
-
1994
- <!-- Loaded world info -->
1995
2857
  <div id="loaded-world-info" style="display:none">
1996
2858
  <div class="loaded-world-card">
1997
2859
  <div class="lw-name" id="lw-name"></div>
@@ -1999,65 +2861,54 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1999
2861
  <div class="lw-stats" id="lw-stats"></div>
2000
2862
  </div>
2001
2863
  </div>
2002
- </div>
2003
-
2004
- <!-- World file schema reference -->
2005
- <div class="ctrl-section">
2006
- <h3>World File Schema</h3>
2007
- <div class="schema-ref">
2008
- <div class="schema-item"><code>name</code> — world name</div>
2009
- <div class="schema-item"><code>thesis</code> — what this world is about</div>
2010
- <div class="schema-item"><code>rules[]</code> — governance rules (plain English or structured)</div>
2011
- <div class="schema-item"><code>invariants[]</code> — rules that always hold <code>{id, description}</code></div>
2012
- <div class="schema-item"><code>gates[]</code> — viability thresholds <code>{id, label, condition, severity}</code></div>
2013
- <div class="schema-item"><code>state_variables[]</code> — sliders <code>{id, label, type, range, default_value}</code></div>
2864
+ <div class="worldfile-hint">
2865
+ Build detailed world files at <strong>neuroverseos.com</strong> or via <code>npm install @neuroverseos/governance</code>
2014
2866
  </div>
2015
2867
  </div>
2016
2868
  </div>
2017
2869
 
2018
- <!-- Simulation Engine (demoted, below world source) -->
2019
- <div class="ctrl-section" style="margin-top:8px">
2020
- <h3>Engine</h3>
2021
- <div class="ctrl-row">
2022
- <select id="engine-select">
2023
- <option value="nv-sim" selected>NV-SIM (Built-in)</option>
2024
- </select>
2870
+ <!-- STEP 2: Connect to Simulator -->
2871
+ <div class="ctrl-section">
2872
+ <h3>Connect to Simulator</h3>
2873
+ <div class="bridge-selector">
2874
+ <button class="bridge-btn" id="bridge-scienceclaw" data-bridge="scienceclaw">
2875
+ <span class="bridge-name">ScienceClaw</span>
2876
+ <span class="bridge-sub">Research governance</span>
2877
+ </button>
2878
+ </div>
2879
+ <div id="bridge-status" class="bridge-status"></div>
2880
+ <div id="bridge-setup" class="bridge-setup" style="display:none">
2881
+ <div id="bridge-setup-content"></div>
2882
+ <div class="bridge-connect-row">
2883
+ <input type="text" id="bridge-endpoint" class="bridge-endpoint-input" placeholder="http://localhost:5001">
2884
+ <button class="btn btn-connect" id="bridge-connect-btn">Connect</button>
2885
+ </div>
2025
2886
  </div>
2026
- <div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
2027
2887
  </div>
2028
2888
 
2029
- <!-- Narrative injection -->
2030
- <div class="ctrl-section">
2031
- <h3>Narrative Events</h3>
2032
- <div class="inject-row">
2033
- <select id="event-select"></select>
2034
- <input type="number" id="event-round" min="1" max="20" value="3" placeholder="R">
2035
- </div>
2036
- <button class="btn btn-add" id="add-event-btn">+ Add Event</button>
2037
- <div id="inject-list" class="inject-list" style="margin-top:8px"></div>
2889
+ <!-- STEP 3: Bridge-specific controls (shown after connection) -->
2890
+ <div class="ctrl-section" id="bridge-controls-section" style="display:none">
2891
+ <h3 id="bridge-controls-label">Settings</h3>
2892
+ <div id="bridge-controls"></div>
2038
2893
  </div>
2039
2894
 
2040
- <!-- Rounds -->
2041
- <div class="ctrl-section">
2042
- <h3>Simulation</h3>
2043
- <div class="ctrl-row">
2044
- <div class="ctrl-label">
2045
- <span>Rounds</span>
2046
- <span class="val" id="rounds-val">5</span>
2047
- </div>
2048
- <input type="range" id="rounds-slider" min="3" max="12" value="5">
2049
- </div>
2895
+ <!-- Starting Conditions (shown when world has state variables) -->
2896
+ <div class="ctrl-section" id="state-vars-section" style="display:none">
2897
+ <h3 style="cursor:pointer" id="state-vars-toggle">
2898
+ <span class="wf-arrow" id="sv-arrow">&#x25B6;</span> Starting Conditions
2899
+ </h3>
2900
+ <div id="state-vars" style="display:none"></div>
2050
2901
  </div>
2051
2902
 
2052
2903
  <!-- Run button -->
2053
- <button class="btn btn-run" id="run-btn">Run Simulation</button>
2904
+ <button class="btn btn-run" id="run-btn">Run Governance</button>
2054
2905
 
2055
- <!-- Save as variant -->
2906
+ <!-- Save as Simulation Rules -->
2056
2907
  <div id="save-section" style="margin-top:12px">
2057
- <button class="btn btn-save" id="save-btn">Save as World Variant</button>
2908
+ <button class="btn btn-save" id="save-btn">Save as Simulation Rules</button>
2058
2909
  <div id="save-form" style="display:none;margin-top:8px">
2059
- <input type="text" id="variant-name" placeholder="Variant name (e.g. Hormuz Closed + 3x Leverage)" class="save-input">
2060
- <input type="text" id="variant-desc" placeholder="What does this variant test?" class="save-input" style="margin-top:4px">
2910
+ <input type="text" id="variant-name" placeholder="Name these rules (e.g. Strict Market Controls)" class="save-input">
2911
+ <input type="text" id="variant-desc" placeholder="What does this ruleset control?" class="save-input" style="margin-top:4px">
2061
2912
  <div style="display:flex;gap:6px;margin-top:6px">
2062
2913
  <button class="btn btn-confirm" id="confirm-save-btn">Save</button>
2063
2914
  <button class="btn btn-cancel" id="cancel-save-btn">Cancel</button>
@@ -2065,48 +2916,28 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
2065
2916
  </div>
2066
2917
  </div>
2067
2918
 
2068
- <!-- Saved variants -->
2919
+ <!-- Saved Simulation Rules -->
2069
2920
  <div class="ctrl-section" style="margin-top:16px">
2070
- <h3>Saved Variants</h3>
2071
- <div id="variant-list"><div style="font-size:11px;color:#333">No saved variants yet</div></div>
2921
+ <h3>Saved Rules</h3>
2922
+ <div id="variant-list"><div style="font-size:11px;color:var(--text-muted)">No saved rules yet</div></div>
2072
2923
  </div>
2073
2924
 
2074
- <!-- Integration Quick-Start -->
2075
- <div class="ctrl-section" style="margin-top:16px">
2076
- <h3>Integrate Your Simulator</h3>
2077
- <div class="integrate-section">
2078
- <h4>Connect in 3 lines</h4>
2079
- <div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate
2080
-
2081
- verdict = evaluate(
2082
- actor=<span class="str">"agent_1"</span>,
2083
- action=<span class="str">"panic_sell"</span>,
2084
- world=<span class="str">"trading"</span>
2085
- )
2086
-
2087
- <span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
2088
- action = <span class="str">"hold"</span> <span class="comment"># adapted</span></div>
2089
- <div class="integrate-endpoint">
2090
- Endpoint: <code id="integrate-url">POST /api/evaluate</code>
2091
- </div>
2092
- <div style="font-size:10px;color:#444;margin-top:6px">
2093
- Fail-open · 500ms timeout · Stateless
2094
- </div>
2095
- <div style="margin-top:8px;font-size:10px">
2096
- <span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> replaced
2097
- <span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> constrained
2098
- <span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
2099
- </div>
2100
- </div>
2925
+ <!-- Post-Run Governance Suggestions -->
2926
+ <div class="ctrl-section" id="governance-suggestions-section" style="display:none;margin-top:16px">
2927
+ <h3>Governance Suggestions</h3>
2928
+ <div id="governance-suggestions" class="governance-suggestions"></div>
2929
+ </div>
2101
2930
 
2102
2931
  <!-- Session Report Panel -->
2103
2932
  <div class="ctrl-section" id="session-panel">
2104
2933
  <h3 class="ctrl-label">SESSION</h3>
2105
- <div class="metric-grid" style="grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
2934
+ <div class="metric-grid" style="grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:8px">
2106
2935
  <div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
2107
2936
  <div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
2108
2937
  <div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
2109
2938
  <div class="metric-box"><div class="value" id="s-allowed" style="color:#4ade80">0</div><div class="label">Allowed</div></div>
2939
+ <div class="metric-box"><div class="value" id="s-rewarded" style="color:#4ade80">0</div><div class="label">Rewarded</div></div>
2940
+ <div class="metric-box"><div class="value" id="s-penalized" style="color:#fb923c">0</div><div class="label">Penalized</div></div>
2110
2941
  </div>
2111
2942
  <div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
2112
2943
  <div style="display:flex;gap:6px;flex-wrap:wrap">
@@ -2119,88 +2950,370 @@ verdict = evaluate(
2119
2950
  </div>
2120
2951
  </div>
2121
2952
 
2122
- <!-- RIGHT: VIEWER -->
2953
+ <!-- RIGHT: OBSERVATION DECK -->
2123
2954
  <div class="viewer">
2124
- <div class="viewer-top">
2125
- <div class="vpanel">
2126
- <h2>Live Metrics</h2>
2127
- <div class="metric-grid">
2128
- <div class="metric-box"><div class="value" id="m-stability">--</div><div class="label">Stability</div></div>
2129
- <div class="metric-box"><div class="value" id="m-volatility">--</div><div class="label">Volatility</div></div>
2130
- <div class="metric-box"><div class="value" id="m-round">--</div><div class="label">Round</div></div>
2131
- <div class="metric-box"><div class="value" id="m-interventions">0</div><div class="label">Interventions</div></div>
2955
+ <div class="deck-header">
2956
+ <h2 class="deck-title">Observation Deck</h2>
2957
+ <div class="deck-status" id="deck-status">Waiting for agents</div>
2958
+ </div>
2959
+
2960
+ <!-- LIVE: Agent Choices — bubble visualization -->
2961
+ <div class="deck-section" id="deck-choices-section">
2962
+ <div class="deck-label">Agent Choices</div>
2963
+ <div class="choice-landscape" id="choice-landscape">
2964
+ <div class="choice-empty" id="choice-empty">
2965
+ <div class="choice-empty-icon">&#x25CC;</div>
2966
+ <div>Agents will appear here when the simulation runs.</div>
2967
+ <div style="margin-top:4px;color:var(--text-faint)">Each bubble = a type of decision. Size = how many agents chose it.</div>
2132
2968
  </div>
2133
2969
  </div>
2134
- <div class="vpanel">
2135
- <h2>World Rules Active</h2>
2136
- <div id="active-invariants"></div>
2137
- </div>
2970
+ <div class="choice-round-label" id="choice-round-label" style="display:none">Round <span id="choice-round-num">1</span></div>
2138
2971
  </div>
2139
2972
 
2140
- <div class="viewer-mid">
2141
- <div class="vpanel" id="agents-panel">
2142
- <h2>Agent Impacts</h2>
2143
- <div id="agents">
2144
- <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>
2145
- </div>
2973
+ <!-- LIVE: Choice Stream — latest agent decisions -->
2974
+ <div class="deck-section" id="deck-stream-section">
2975
+ <div class="deck-label">
2976
+ <span>Live Decisions</span>
2977
+ <span class="stream-count" id="stream-count"></span>
2146
2978
  </div>
2147
- <div class="vpanel">
2148
- <h2>Impact Timeline</h2>
2149
- <div class="chart-container"><canvas id="chart"></canvas></div>
2979
+ <div class="choice-stream" id="choice-stream">
2980
+ <div class="stream-empty">Agent decisions will stream here in real time</div>
2150
2981
  </div>
2151
2982
  </div>
2152
2983
 
2153
- <!-- System Shift Cardthe demo moment -->
2154
- <div id="system-shift" class="system-shift">
2155
- <div class="ss-header">
2156
- <div class="ss-icon"></div>
2157
- <span class="ss-title">System Shift</span>
2984
+ <!-- AFTER: Behavioral Driftfirst vs last round -->
2985
+ <div class="deck-section" id="deck-drift-section" style="display:none">
2986
+ <div class="deck-label">Behavioral Drift</div>
2987
+ <div class="drift-verdict" id="drift-verdict"></div>
2988
+ <div class="drift-comparison" id="drift-comparison"></div>
2989
+ </div>
2990
+
2991
+ <!-- AFTER: Incentive Tracker (Reward/Penalize) — shown when governance engine supports it -->
2992
+ <div class="deck-section" id="deck-incentive-section" style="display:none">
2993
+ <div class="deck-label">Incentive Tracker</div>
2994
+ <div class="incentive-summary" id="incentive-summary"></div>
2995
+ <div class="incentive-agents" id="incentive-agents"></div>
2996
+ </div>
2997
+
2998
+ <!-- AFTER: What-If counterfactual -->
2999
+ <div class="deck-section" id="deck-whatif-section" style="display:none">
3000
+ <div class="deck-label">What If There Were No Rules?</div>
3001
+ <div class="whatif-content" id="whatif-content"></div>
3002
+ </div>
3003
+
3004
+ <!-- EXPORT -->
3005
+ <div class="export-bar">
3006
+ <button class="export-btn" onclick="exportPDF()">Download PDF</button>
3007
+ <button class="export-btn" onclick="exportCSV()">Export CSV</button>
3008
+ <button class="export-btn" onclick="copyShareSummary()">Copy Summary</button>
3009
+ <button class="export-btn audit-btn" onclick="openAudit()">Audit Trail</button>
3010
+ </div>
3011
+
3012
+ <!-- Hidden elements for data tracking (not displayed) -->
3013
+ <div style="display:none">
3014
+ <span id="m-stability">--</span>
3015
+ <span id="m-volatility">--</span>
3016
+ <span id="m-round">--</span>
3017
+ <span id="m-interventions">0</span>
3018
+ <span id="trace-source"></span>
3019
+ <div id="agents"></div>
3020
+ <div id="active-invariants"></div>
3021
+ <div id="log"></div>
3022
+ </div>
3023
+ </div>
3024
+
3025
+ <!-- LAYER 5: AUDIT OVERLAY (separate from dashboard) -->
3026
+ <div class="audit-overlay" id="audit-overlay" onclick="if(event.target===this)closeAudit()">
3027
+ <div class="audit-modal">
3028
+ <div class="audit-header">
3029
+ <h2>Audit Trail</h2>
3030
+ <button class="audit-close" onclick="closeAudit()">Close</button>
2158
3031
  </div>
2159
- <div class="ss-rule" id="ss-rule"></div>
2160
- <div class="ss-scale" id="ss-scale"></div>
2161
- <div class="ss-flow">
2162
- <span>Rule</span><span class="ss-flow-arrow">→</span>
2163
- <span>Behavioral Shift</span><span class="ss-flow-arrow">→</span>
2164
- <span>Emergent Pattern</span><span class="ss-flow-arrow">→</span>
2165
- <span>System Outcome</span>
3032
+ <div class="audit-section" id="audit-rules">
3033
+ <h3>Active Rules</h3>
3034
+ <div id="audit-rules-content"></div>
2166
3035
  </div>
2167
- <div class="ss-body">
2168
- <div class="ss-section">
2169
- <div class="ss-section-label">Behavioral Shift</div>
2170
- <div class="ss-adapt-rate" id="ss-adapt-rate"></div>
2171
- <div class="ss-adapt-desc" id="ss-adapt-desc"></div>
2172
- <div id="ss-shifts"></div>
3036
+ <div class="audit-section" id="audit-verdicts">
3037
+ <h3>Verdict Log</h3>
3038
+ <div id="audit-verdicts-content"></div>
3039
+ </div>
3040
+ <div class="audit-section" id="audit-trace">
3041
+ <h3>Detailed Trace</h3>
3042
+ <div id="audit-trace-content"></div>
3043
+ </div>
3044
+ </div>
3045
+ </div>
3046
+ </div>
3047
+
3048
+ <!-- ONBOARDING OVERLAY (first visit) -->
3049
+ <div class="onboard-overlay" id="onboard-overlay" style="display:none">
3050
+ <div class="onboard-card">
3051
+ <div class="onboard-header">
3052
+ <h2>NeuroVerse Governance Runtime</h2>
3053
+ <div class="onboard-tagline">
3054
+ Define what your AI agents <em>cannot</em> do. Watch what happens when they try.
3055
+ </div>
3056
+ </div>
3057
+ <div class="onboard-body">
3058
+
3059
+ <div class="onboard-step">
3060
+ <div class="onboard-step-num">1</div>
3061
+ <div class="onboard-step-content">
3062
+ <h4>Say what you want to control</h4>
3063
+ <p>Type plain English. No config files, no YAML, no code.</p>
2173
3064
  </div>
2174
- <div class="ss-section">
2175
- <div class="ss-section-label">What Emerged</div>
2176
- <div id="ss-patterns"></div>
3065
+ </div>
3066
+
3067
+ <div class="onboard-example">
3068
+ <div class="onboard-example-label">Example</div>
3069
+ <div class="onboard-example-text">"No agent may sell more than 10% of its portfolio in a single round. Block any agent that attempts to front-run another's trade."</div>
3070
+ <div class="onboard-example-result">
3071
+ <strong>&#x2192;</strong> Generates 2 governance rules: a <span style="color:#f87171">GATE</span> on portfolio concentration and a <span style="color:#f87171">BLOCK</span> on front-running patterns. These rules gate agent invocations at entry points before they execute.
2177
3072
  </div>
2178
- <div class="ss-section">
2179
- <div class="ss-section-label">System Outcome</div>
2180
- <div id="ss-impacts"></div>
3073
+ </div>
3074
+
3075
+ <div class="onboard-step">
3076
+ <div class="onboard-step-num">2</div>
3077
+ <div class="onboard-step-content">
3078
+ <h4>Connect your simulator</h4>
3079
+ <p>Plug in ScienceClaw (research governance) or any simulator. Governance works with any system that sends actions to the <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px;font-size:10px">/api/evaluate</code> endpoint.</p>
2181
3080
  </div>
2182
- <div class="ss-section">
2183
- <div class="ss-section-label">What Actually Happened</div>
2184
- <div class="ss-narrative" id="ss-narrative"></div>
3081
+ </div>
3082
+
3083
+ <div class="onboard-step">
3084
+ <div class="onboard-step-num">3</div>
3085
+ <div class="onboard-step-content">
3086
+ <h4>Run and see what emerges</h4>
3087
+ <p>Agents act. Rules intervene. The right panel shows you <strong>what happened</strong>, <strong>what agents did</strong>, and <strong>why</strong> &mdash; not charts.</p>
2185
3088
  </div>
2186
3089
  </div>
2187
- <button class="ss-raw-toggle" id="ss-raw-toggle">
2188
- <span class="arrow">▶</span> View raw detail
2189
- </button>
2190
- <div class="ss-raw-detail" id="ss-raw-detail">
2191
- <div class="ss-raw-list" id="ss-raw-list"></div>
3090
+
3091
+ <div class="onboard-section">
3092
+ <h4>What you get</h4>
3093
+ <div class="onboard-capability"><span class="cap-icon">&#x25CF;</span> <span><strong>Behavioral reports</strong> &mdash; "7 agents attempted panic sells, all blocked. Stability improved 34 points." Plain language, not dashboards.</span></div>
3094
+ <div class="onboard-capability"><span class="cap-icon">&#x25CF;</span> <span><strong>Full audit trail</strong> &mdash; Every action, every verdict, every rule that fired. Exportable as CSV or PDF.</span></div>
3095
+ <div class="onboard-capability"><span class="cap-icon">&#x25CF;</span> <span><strong>Shape emergence</strong> &mdash; Change one rule, re-run, see how 1000 agents behave differently. Your rules define the boundaries where complex behavior can safely happen.</span></div>
3096
+ <div class="onboard-capability"><span class="cap-icon">&#x25CF;</span> <span><strong>Save &amp; compare</strong> &mdash; Save rule configurations, reload them, compare results across runs.</span></div>
2192
3097
  </div>
2193
3098
  </div>
2194
-
2195
- <div class="viewer-bottom">
2196
- <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>
2197
- <div id="log"></div>
3099
+ <div class="onboard-footer">
3100
+ <button class="btn-onboard-start" id="onboard-start-btn">Start</button>
3101
+ <span class="onboard-dismiss" id="onboard-dismiss">Don't show again</span>
2198
3102
  </div>
2199
3103
  </div>
2200
3104
  </div>
2201
3105
 
2202
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"><\/script>
2203
3106
  <script>
3107
+ // ============================================
3108
+ // OUTCOME INTELLIGENCE LAYER
3109
+ // ============================================
3110
+
3111
+ function generateOutcome(stability, volatility, shiftData, worldThesis) {
3112
+ var st = stability || 0;
3113
+ var vol = volatility || 0;
3114
+ var blockRatio = shiftData.total > 0 ? shiftData.blocks / shiftData.total : 0;
3115
+ var thesis = worldThesis || '';
3116
+
3117
+ // Determine outcome direction + dominant behavior (state + what agents did)
3118
+ var direction = '';
3119
+ var behavior = '';
3120
+ if (st > 0.7 && vol < 0.3) {
3121
+ direction = 'stabilized';
3122
+ behavior = 'agents shifted toward safer positions';
3123
+ } else if (st > 0.7 && vol >= 0.3) {
3124
+ direction = 'held under pressure';
3125
+ behavior = 'agents maintained cautious strategies despite volatility';
3126
+ } else if (st > 0.4 && vol < 0.5) {
3127
+ direction = 'partially converged';
3128
+ behavior = 'agents split between aggressive and conservative approaches';
3129
+ } else if (st <= 0.4 && vol >= 0.5) {
3130
+ direction = 'fragmented';
3131
+ behavior = 'agents competed with conflicting strategies';
3132
+ } else if (st <= 0.4) {
3133
+ direction = 'remains uncertain';
3134
+ behavior = 'agents failed to find a dominant strategy';
3135
+ } else {
3136
+ direction = 'showed mixed results';
3137
+ behavior = 'agents oscillated between risk-taking and caution';
3138
+ }
3139
+
3140
+ // Enrich with world context if available
3141
+ if (thesis) {
3142
+ var nouns = thesis.match(/\b(market|research|supply|trade|financial|climate|regulatory|investment|innovation|security)\b/i);
3143
+ if (nouns) {
3144
+ var subject = nouns[1].charAt(0).toUpperCase() + nouns[1].slice(1).toLowerCase();
3145
+ return subject + ' ' + direction + ' as ' + behavior;
3146
+ }
3147
+ }
3148
+
3149
+ // Build behavior-enriched statement — always state + dominant behavior
3150
+ if (blockRatio > 0.4 && st > 0.6) return 'System ' + direction + ' after agents abandoned high-risk strategies';
3151
+ if (blockRatio > 0.2) return 'Outcome ' + direction + ' as ' + behavior;
3152
+ if (shiftData.patterns && shiftData.patterns.length > 0) return 'Outcome ' + direction + ' — ' + behavior;
3153
+ return 'Outcome ' + direction + ' as ' + behavior;
3154
+ }
3155
+
3156
+ function computeConfidence(stability, volatility, interventions, total) {
3157
+ var interventionRate = total > 0 ? interventions / total : 0;
3158
+ var score = stability * 0.5 + (1 - volatility) * 0.3 + (1 - interventionRate) * 0.2;
3159
+
3160
+ var strength, evidence, risk, strengthCls, evidenceCls, riskCls;
3161
+
3162
+ if (score > 0.75) {
3163
+ strength = 'Strong'; strengthCls = 'good';
3164
+ evidence = 'Solid'; evidenceCls = 'good';
3165
+ risk = 'Low'; riskCls = 'good';
3166
+ } else if (score > 0.55) {
3167
+ strength = 'Moderate'; strengthCls = 'warn';
3168
+ evidence = stability > 0.6 ? 'Solid' : 'Mixed'; evidenceCls = stability > 0.6 ? 'good' : 'warn';
3169
+ risk = volatility > 0.4 ? 'Elevated' : 'Moderate'; riskCls = volatility > 0.4 ? 'warn' : 'warn';
3170
+ } else if (score > 0.35) {
3171
+ strength = 'Moderate'; strengthCls = 'warn';
3172
+ evidence = 'Mixed'; evidenceCls = 'warn';
3173
+ risk = 'Elevated'; riskCls = 'bad';
3174
+ } else {
3175
+ strength = 'Weak'; strengthCls = 'bad';
3176
+ evidence = 'Thin'; evidenceCls = 'bad';
3177
+ risk = 'High'; riskCls = 'bad';
3178
+ }
3179
+
3180
+ return { score: score, strength: strength, strengthCls: strengthCls, evidence: evidence, evidenceCls: evidenceCls, risk: risk, riskCls: riskCls };
3181
+ }
3182
+
3183
+ function translateBehaviorNarrative(reaction, verdict) {
3184
+ // Convert verdict+action into before → after narrative — no system words
3185
+ var status = verdict ? verdict.status : 'ALLOW';
3186
+ var action = reaction || 'acted';
3187
+ if (status === 'BLOCK') {
3188
+ return 'abandoned ' + action + ' and switched to a safer strategy';
3189
+ }
3190
+ if (status === 'PENALIZE') {
3191
+ return 'was penalized for ' + action + ' and frozen for cooldown';
3192
+ }
3193
+ if (status === 'REWARD') {
3194
+ return 'was rewarded for ' + action + ' — behavior reinforced';
3195
+ }
3196
+ if (status === 'MODIFY' || status === 'PAUSE') {
3197
+ return 'scaled back ' + action + ' after early resistance';
3198
+ }
3199
+ return action;
3200
+ }
3201
+
3202
+ function generateBehaviorShifts(shiftData, bLog) {
3203
+ // Quantified behavioral shifts in human language
3204
+ var sentences = [];
3205
+ var total = shiftData.total || bLog.length || 0;
3206
+ if (total === 0) return sentences;
3207
+
3208
+ // Group by what agents actually did — always anchor in before → after
3209
+ var keys = Object.keys(shiftData.shifts || {}).sort(function(a, b) { return shiftData.shifts[b] - shiftData.shifts[a]; });
3210
+ if (keys.length > 0 && total > 0) {
3211
+ keys.slice(0, 3).forEach(function(k) {
3212
+ var parts = k.split(': ');
3213
+ var action = parts[1] || parts[0];
3214
+ var count = shiftData.shifts[k];
3215
+ var pct = Math.round((count / total) * 100);
3216
+ // before → after: what they tried → what they did instead
3217
+ if (parts[0] === 'BLOCK') {
3218
+ sentences.push(pct + '% of agents (' + count + ') shifted from ' + action + ' to conservative strategies');
3219
+ } else {
3220
+ sentences.push(pct + '% of agents (' + count + ') reduced ' + action + ' after initial attempts failed');
3221
+ }
3222
+ });
3223
+ }
3224
+
3225
+ // Fallback from behavioral log — still before → after
3226
+ if (sentences.length === 0 && bLog.length > 0) {
3227
+ var blocked = bLog.filter(function(e) { return e.status === 'BLOCK'; }).length;
3228
+ var modified = bLog.filter(function(e) { return e.status === 'MODIFY' || e.status === 'PAUSE'; }).length;
3229
+ var penalized = bLog.filter(function(e) { return e.status === 'PENALIZE'; }).length;
3230
+ var rewarded = bLog.filter(function(e) { return e.status === 'REWARD'; }).length;
3231
+ var proceeded = bLog.length - blocked - modified - penalized;
3232
+
3233
+ if (blocked > 0) {
3234
+ var bPct = Math.round((blocked / bLog.length) * 100);
3235
+ sentences.push(bPct + '% of agents (' + blocked + ') shifted from aggressive to conservative strategies');
3236
+ }
3237
+ if (modified > 0) {
3238
+ var mPct = Math.round((modified / bLog.length) * 100);
3239
+ sentences.push(mPct + '% of agents (' + modified + ') reduced position size after initial attempts failed');
3240
+ }
3241
+ if (penalized > 0) {
3242
+ var penPct = Math.round((penalized / bLog.length) * 100);
3243
+ sentences.push(penPct + '% of agents (' + penalized + ') were penalized and frozen for cooldown periods');
3244
+ }
3245
+ if (rewarded > 0) {
3246
+ var rewPct = Math.round((rewarded / bLog.length) * 100);
3247
+ sentences.push(rewPct + '% of agents (' + rewarded + ') were rewarded for desirable behavior');
3248
+ }
3249
+ if (proceeded > 0 && (blocked > 0 || modified > 0 || penalized > 0)) {
3250
+ var pPct = Math.round((proceeded / bLog.length) * 100);
3251
+ sentences.push(pPct + '% of agents (' + proceeded + ') maintained their original strategy throughout');
3252
+ }
3253
+ }
3254
+
3255
+ // Volatility insight — before → after framing
3256
+ if (shiftData.governedVol < shiftData.baselineVol && shiftData.baselineVol > 0) {
3257
+ var volDrop = Math.round((1 - shiftData.governedVol / shiftData.baselineVol) * 100);
3258
+ sentences.push('Uncertainty dropped ' + volDrop + '% as agents moved from exploration to caution');
3259
+ }
3260
+
3261
+ // Add emergent pattern insights
3262
+ if (shiftData.patterns && shiftData.patterns.length > 0) {
3263
+ shiftData.patterns.slice(0, 2).forEach(function(p) {
3264
+ sentences.push(p);
3265
+ });
3266
+ }
3267
+
3268
+ return sentences;
3269
+ }
3270
+
3271
+ function generateCausation(shiftData, bLog) {
3272
+ // Pure causation — why behavior changed — no governance terminology
3273
+ var causes = [];
3274
+ var total = shiftData.total || bLog.length || 0;
3275
+ if (total === 0) return causes;
3276
+
3277
+ var blockRatio = shiftData.blocks / total;
3278
+
3279
+ // No system words (feedback, evaluation, validation, review)
3280
+ // Only agent experience (failed attempts, uncertainty, risk, delay)
3281
+ if (blockRatio > 0.4) {
3282
+ causes.push('Early aggressive attempts failed, forcing agents to rethink their strategy');
3283
+ } else if (blockRatio > 0.1) {
3284
+ causes.push('Some agents hit unexpected resistance and pulled back');
3285
+ }
3286
+
3287
+ if (shiftData.governedVol < shiftData.baselineVol) {
3288
+ causes.push('Uncertainty dropped as agents stopped experimenting and committed to safer positions');
3289
+ }
3290
+
3291
+ if (shiftData.patterns && shiftData.patterns.length > 0) {
3292
+ if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('hold') || p.toLowerCase().includes('caution'); })) {
3293
+ causes.push('Risk became too visible — agents chose to wait rather than act');
3294
+ }
3295
+ if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('coordination') || p.toLowerCase().includes('coordinated'); })) {
3296
+ causes.push('Agents independently converged on similar strategies under shared pressure');
3297
+ }
3298
+ }
3299
+
3300
+ // Detect adaptation pattern from behavior log
3301
+ if (bLog.length >= 3) {
3302
+ var recent = bLog.slice(-6);
3303
+ var earlyBlocks = recent.slice(0, 3).filter(function(e) { return e.status === 'BLOCK'; }).length;
3304
+ var lateAllows = recent.slice(-3).filter(function(e) { return e.status === 'ALLOW'; }).length;
3305
+ if (earlyBlocks >= 2 && lateAllows >= 2) {
3306
+ causes.push('Agents became more cautious after early attempts failed');
3307
+ }
3308
+ }
3309
+
3310
+ if (causes.length === 0) {
3311
+ causes.push('Agents held steady — no major shifts in strategy throughout the simulation');
3312
+ }
3313
+
3314
+ return causes;
3315
+ }
3316
+
2204
3317
  // ============================================
2205
3318
  // STATE
2206
3319
  // ============================================
@@ -2210,88 +3323,129 @@ let narratives = {};
2210
3323
  let currentWorld = null;
2211
3324
  let injectedEvents = [];
2212
3325
  let totalInterventions = 0;
2213
- let baselineImpacts = [];
2214
- let governedImpacts = [];
2215
- let chartLabels = [];
2216
- let chart = null;
3326
+ let totalActions = 0;
2217
3327
  let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
2218
3328
  let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
3329
+ let behaviorLog = []; // { agent, action, status, reason, ts }
3330
+ let latestStability = 0;
3331
+ let latestVolatility = 0;
3332
+
3333
+ // Bridge state
3334
+ let connectedBridge = null; // { id, label, capabilities, endpoint }
3335
+ let currentThesis = ''; // What the user is trying to control
3336
+ let currentRules = []; // Generated governance rules
3337
+ let bridgeCapabilities = {}; // { scienceclaw: {...} }
2219
3338
 
2220
3339
  const statusEl = document.getElementById('status');
2221
- const worldSelect = document.getElementById('world-select');
2222
3340
  const stateVarsSection = document.getElementById('state-vars-section');
2223
3341
  const stateVarsEl = document.getElementById('state-vars');
2224
- const scenarioListEl = document.getElementById('scenario-list');
2225
- const eventSelect = document.getElementById('event-select');
2226
- const eventRoundInput = document.getElementById('event-round');
2227
- const injectListEl = document.getElementById('inject-list');
2228
- const roundsSlider = document.getElementById('rounds-slider');
2229
- const roundsVal = document.getElementById('rounds-val');
2230
3342
  const runBtn = document.getElementById('run-btn');
2231
3343
  const agentsEl = document.getElementById('agents');
2232
3344
  const logEl = document.getElementById('log');
2233
3345
  const activeInvEl = document.getElementById('active-invariants');
2234
- const engineSelect = document.getElementById('engine-select');
2235
- const engineStatusEl = document.getElementById('engine-status');
2236
3346
  const traceSourceEl = document.getElementById('trace-source');
3347
+ const outcomeStatementEl = document.getElementById('outcome-statement');
3348
+ const confidenceGridEl = document.getElementById('confidence-grid');
3349
+ const outcomeContextEl = document.getElementById('outcome-context');
3350
+ const behaviorShiftsEl = document.getElementById('behavior-shifts');
3351
+ const activityTimelineEl = document.getElementById('activity-timeline');
3352
+ const activityToggleEl = document.getElementById('activity-toggle');
3353
+ const whyContentEl = document.getElementById('why-content');
3354
+
3355
+ // Audit + Export
3356
+ function openAudit() {
3357
+ // Populate audit content from current data
3358
+ var rulesEl = document.getElementById('audit-rules-content');
3359
+ rulesEl.innerHTML = activeInvEl.innerHTML || '<div style="font-size:11px;color:#666">No rules loaded</div>';
3360
+
3361
+ var verdictsEl = document.getElementById('audit-verdicts-content');
3362
+ var vHtml = '';
3363
+ behaviorLog.forEach(function(e) {
3364
+ vHtml += '<div style="font-size:10px;padding:2px 0;display:flex;gap:8px;border-bottom:1px solid var(--border)">' +
3365
+ '<span style="color:var(--blue);min-width:100px">' + e.agent + '</span>' +
3366
+ '<span style="flex:1;color:var(--text-secondary)">' + e.action + '</span>' +
3367
+ '<span style="font-size:9px;font-weight:600;padding:0 4px;border-radius:2px;' +
3368
+ (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>' +
3369
+ (e.reason ? '<span style="font-size:9px;color:var(--text-faint)">' + e.reason + '</span>' : '') +
3370
+ '</div>';
3371
+ });
3372
+ verdictsEl.innerHTML = vHtml || '<div style="font-size:11px;color:#666">No verdict data</div>';
3373
+
3374
+ var traceEl = document.getElementById('audit-trace-content');
3375
+ traceEl.innerHTML = logEl.innerHTML || '<div style="font-size:11px;color:#666">No trace data</div>';
3376
+
3377
+ document.getElementById('audit-overlay').classList.add('open');
3378
+ }
3379
+
3380
+ function closeAudit() {
3381
+ document.getElementById('audit-overlay').classList.remove('open');
3382
+ }
3383
+
3384
+ function exportPDF() {
3385
+ window.print();
3386
+ }
3387
+
3388
+ function exportCSV() {
3389
+ if (behaviorLog.length === 0) { alert('No data to export yet'); return; }
3390
+ var csv = 'Agent,Action,Behavior,Round\\n';
3391
+ behaviorLog.forEach(function(e) {
3392
+ var behavior = translateBehaviorNarrative(e.action, { status: e.status });
3393
+ csv += '"' + e.agent + '","' + e.action + '","' + behavior + '","' + (e.round || '') + '"\\n';
3394
+ });
3395
+ var blob = new Blob([csv], { type: 'text/csv' });
3396
+ var url = URL.createObjectURL(blob);
3397
+ var a = document.createElement('a');
3398
+ a.href = url;
3399
+ a.download = 'simulation-behavior-' + new Date().toISOString().slice(0, 10) + '.csv';
3400
+ a.click();
3401
+ URL.revokeObjectURL(url);
3402
+ }
3403
+
3404
+ function copyShareSummary() {
3405
+ var text = 'OBSERVATION DECK SUMMARY\\n\\n';
3406
+
3407
+ // Drift verdict
3408
+ var driftVerdict = document.getElementById('drift-verdict');
3409
+ if (driftVerdict && driftVerdict.textContent) {
3410
+ text += 'BEHAVIORAL DRIFT: ' + driftVerdict.textContent + '\\n\\n';
3411
+ }
3412
+
3413
+ // What-if
3414
+ var whatifContent = document.getElementById('whatif-content');
3415
+ if (whatifContent && whatifContent.textContent) {
3416
+ text += 'COUNTERFACTUAL: ' + whatifContent.textContent + '\\n\\n';
3417
+ }
3418
+
3419
+ // Basic stats
3420
+ text += 'Total agent actions: ' + totalActions + '\\n';
3421
+ text += 'Interventions: ' + totalInterventions + '\\n';
3422
+ text += '\\nGenerated by NeuroVerse Governance Runtime';
3423
+
3424
+ navigator.clipboard.writeText(text).then(function() {
3425
+ alert('Summary copied to clipboard');
3426
+ });
3427
+ }
2237
3428
 
2238
3429
  // ============================================
2239
- // INIT — Load worlds, scenarios, narratives, adapters
3430
+ // INIT — Load bridge capabilities and saved rules
2240
3431
  // ============================================
2241
3432
  async function init() {
2242
- const [wRes, sRes, nRes, aRes] = await Promise.all([
2243
- fetch('/api/worlds').then(r => r.json()),
2244
- fetch('/api/scenarios').then(r => r.json()),
2245
- fetch('/api/narratives').then(r => r.json()),
2246
- fetch('/api/adapters').then(r => r.json()).catch(() => ({ adapters: [] })),
3433
+ // Load bridge capabilities and worlds (for file uploads that reference presets)
3434
+ const [bcRes, wRes] = await Promise.all([
3435
+ fetch('/api/bridge-capabilities').then(r => r.json()).catch(() => ({ bridges: [] })),
3436
+ fetch('/api/worlds').then(r => r.json()).catch(() => ({ worlds: [] })),
2247
3437
  ]);
2248
3438
 
2249
3439
  worlds = wRes.worlds;
2250
- scenarios = sRes.scenarios;
2251
- narratives = nRes.narratives;
2252
-
2253
- // Populate engine selector with live adapters
2254
- (aRes.adapters || []).forEach(function(a) {
2255
- const opt = document.createElement('option');
2256
- opt.value = a.id;
2257
- opt.textContent = a.label;
2258
- engineSelect.appendChild(opt);
2259
- });
2260
3440
 
2261
- // Populate world select
2262
- worlds.forEach(w => {
2263
- const opt = document.createElement('option');
2264
- opt.value = w.id;
2265
- opt.textContent = w.title;
2266
- worldSelect.appendChild(opt);
3441
+ // Index bridge capabilities
3442
+ (bcRes.bridges || []).forEach(function(b) {
3443
+ bridgeCapabilities[b.id] = b;
2267
3444
  });
2268
3445
 
2269
- // Populate event select
2270
- Object.entries(narratives).forEach(([id, ev]) => {
2271
- const opt = document.createElement('option');
2272
- opt.value = id;
2273
- opt.textContent = ev.headline.slice(0, 40);
2274
- eventSelect.appendChild(opt);
2275
- });
2276
-
2277
- // Populate scenario presets
2278
- Object.entries(scenarios).forEach(([id, s]) => {
2279
- const btn = document.createElement('button');
2280
- btn.className = 'scenario-btn';
2281
- btn.innerHTML = '<div class="stitle">' + s.title + '</div><div class="sdesc">' + s.description + '</div>';
2282
- btn.onclick = () => loadScenario(id, s);
2283
- scenarioListEl.appendChild(btn);
2284
- });
2285
-
2286
- // Select first world
2287
- if (worlds.length > 0) selectWorld(worlds[0].id);
2288
-
2289
- // Load saved variants
3446
+ // Load saved rules
2290
3447
  await loadVariants();
2291
3448
 
2292
- // Populate base world selector for custom rules mode
2293
- populateBaseWorldSelect();
2294
-
2295
3449
  // Connect SSE
2296
3450
  connectSSE();
2297
3451
  }
@@ -2300,114 +3454,384 @@ function selectWorld(worldId) {
2300
3454
  currentWorld = worlds.find(w => w.id === worldId);
2301
3455
  if (!currentWorld) return;
2302
3456
 
2303
- worldSelect.value = worldId;
2304
- document.getElementById('world-thesis').textContent = '"' + currentWorld.thesis + '"';
3457
+ // Render state variable controls if world has them
3458
+ renderStateVars();
3459
+ }
2305
3460
 
2306
- // Render state variable controls
2307
- if (currentWorld.stateVariables && currentWorld.stateVariables.length > 0) {
2308
- stateVarsSection.style.display = '';
2309
- stateVarsEl.innerHTML = '';
2310
- currentWorld.stateVariables.forEach(sv => {
2311
- const row = document.createElement('div');
2312
- row.className = 'ctrl-row';
2313
-
2314
- if (sv.type === 'number' && sv.range) {
2315
- const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
2316
- row.innerHTML =
2317
- '<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
2318
- '<input type="range" id="sv-' + sv.id + '" min="' + sv.range.min + '" max="' + sv.range.max + '" step="' + step + '" value="' + sv.default_value + '" data-sv="' + sv.id + '">';
2319
- stateVarsEl.appendChild(row);
2320
- const slider = row.querySelector('input');
2321
- slider.addEventListener('input', () => {
2322
- document.getElementById('sv-val-' + sv.id).textContent = slider.value;
2323
- });
2324
- } else if (sv.type === 'enum' && sv.enum_values) {
2325
- row.innerHTML =
2326
- '<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
2327
- '<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
2328
- sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
2329
- '</select>';
2330
- stateVarsEl.appendChild(row);
2331
- } else if (sv.type === 'boolean') {
2332
- row.innerHTML =
2333
- '<div class="toggle-row">' +
2334
- '<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
2335
- '<span class="toggle-label">' + sv.label + '</span>' +
2336
- '</div>';
2337
- stateVarsEl.appendChild(row);
2338
- const toggle = row.querySelector('.toggle');
2339
- toggle.addEventListener('click', () => {
2340
- toggle.classList.toggle('on');
2341
- });
2342
- }
2343
- });
2344
- } else {
3461
+ function renderStateVars() {
3462
+ if (!currentWorld || !currentWorld.stateVariables || currentWorld.stateVariables.length === 0) {
2345
3463
  stateVarsSection.style.display = 'none';
3464
+ return;
2346
3465
  }
2347
-
2348
- // Show invariants
2349
- activeInvEl.innerHTML = currentWorld.invariants.map(inv =>
2350
- '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
2351
- ).join('') + (currentWorld.gates || []).map(g =>
2352
- '<div class="inv-item" style="color:' + (g.severity === 'critical' ? '#f87171' : '#fbbf24') + '">[' + g.id + '] ' + g.label + '</div>'
2353
- ).join('');
3466
+ stateVarsSection.style.display = '';
3467
+ stateVarsEl.innerHTML = '';
3468
+ stateVarsEl.style.display = 'none'; // collapsed by default
3469
+
3470
+ currentWorld.stateVariables.forEach(sv => {
3471
+ const row = document.createElement('div');
3472
+ row.className = 'ctrl-row';
3473
+
3474
+ if (sv.type === 'number' && sv.range) {
3475
+ const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
3476
+ row.innerHTML =
3477
+ '<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
3478
+ '<input type="range" id="sv-' + sv.id + '" min="' + sv.range.min + '" max="' + sv.range.max + '" step="' + step + '" value="' + sv.default_value + '" data-sv="' + sv.id + '">';
3479
+ stateVarsEl.appendChild(row);
3480
+ const slider = row.querySelector('input');
3481
+ slider.addEventListener('input', () => {
3482
+ document.getElementById('sv-val-' + sv.id).textContent = slider.value;
3483
+ });
3484
+ } else if (sv.type === 'enum' && sv.enum_values) {
3485
+ row.innerHTML =
3486
+ '<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
3487
+ '<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
3488
+ sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
3489
+ '</select>';
3490
+ stateVarsEl.appendChild(row);
3491
+ } else if (sv.type === 'boolean') {
3492
+ row.innerHTML =
3493
+ '<div class="toggle-row">' +
3494
+ '<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
3495
+ '<span class="toggle-label">' + sv.label + '</span>' +
3496
+ '</div>';
3497
+ stateVarsEl.appendChild(row);
3498
+ const toggle = row.querySelector('.toggle');
3499
+ toggle.addEventListener('click', () => {
3500
+ toggle.classList.toggle('on');
3501
+ });
3502
+ }
3503
+ });
2354
3504
  }
2355
3505
 
2356
- function loadScenario(id, scenario) {
2357
- // Set world
2358
- selectWorld(scenario.world);
2359
- // Set events
2360
- injectedEvents = scenario.events.slice();
2361
- renderInjectedEvents();
2362
- // Set rounds
2363
- const r = scenario.rounds || 5;
2364
- roundsSlider.value = Math.min(r, 12);
2365
- roundsVal.textContent = Math.min(r, 12);
2366
- }
3506
+ // State vars toggle (collapsed by default)
3507
+ document.getElementById('state-vars-toggle').addEventListener('click', () => {
3508
+ const el = stateVarsEl;
3509
+ const arrow = document.getElementById('sv-arrow');
3510
+ if (el.style.display === 'none') {
3511
+ el.style.display = '';
3512
+ arrow.innerHTML = '&#x25BC;';
3513
+ } else {
3514
+ el.style.display = 'none';
3515
+ arrow.innerHTML = '&#x25B6;';
3516
+ }
3517
+ });
2367
3518
 
2368
3519
  // ============================================
2369
- // NARRATIVE EVENT INJECTION
3520
+ // THESIS INPUT — Generate Governance Rules
2370
3521
  // ============================================
2371
- document.getElementById('add-event-btn').addEventListener('click', () => {
2372
- const eventId = eventSelect.value;
2373
- const round = parseInt(eventRoundInput.value);
2374
- if (!eventId || isNaN(round)) return;
2375
- injectedEvents.push(eventId + '@' + round);
2376
- renderInjectedEvents();
2377
- });
3522
+ const thesisInput = document.getElementById('thesis-input');
3523
+ const generateRulesBtn = document.getElementById('generate-rules-btn');
3524
+ const ruleGenStatusEl = document.getElementById('rule-generation-status');
3525
+ const generatedRulesPreviewEl = document.getElementById('generated-rules-preview');
3526
+
3527
+ generateRulesBtn.addEventListener('click', async () => {
3528
+ const thesis = thesisInput.value.trim();
3529
+ if (!thesis) return;
3530
+
3531
+ currentThesis = thesis;
3532
+ generateRulesBtn.disabled = true;
3533
+ generateRulesBtn.textContent = 'Generating...';
3534
+ ruleGenStatusEl.textContent = '';
2378
3535
 
2379
- function renderInjectedEvents() {
2380
- injectListEl.innerHTML = injectedEvents.map((ev, i) =>
2381
- '<div class="inject-item"><span>' + ev + '</span><span class="remove" data-idx="' + i + '">x</span></div>'
2382
- ).join('');
2383
- injectListEl.querySelectorAll('.remove').forEach(el => {
2384
- el.addEventListener('click', () => {
2385
- injectedEvents.splice(parseInt(el.dataset.idx), 1);
2386
- renderInjectedEvents();
3536
+ try {
3537
+ const resp = await fetch('/api/generate-rules', {
3538
+ method: 'POST',
3539
+ headers: { 'Content-Type': 'application/json' },
3540
+ body: JSON.stringify({ thesis }),
2387
3541
  });
2388
- });
2389
- }
3542
+ const data = await resp.json();
3543
+
3544
+ if (data.error) {
3545
+ ruleGenStatusEl.textContent = data.error;
3546
+ ruleGenStatusEl.className = 'rule-status error';
3547
+ } else {
3548
+ currentRules = data.parsed.rules || [];
3549
+ ruleGenStatusEl.textContent = data.parsed.total + ' governance rule' + (data.parsed.total !== 1 ? 's' : '') + ' generated. Ready to run.';
3550
+ ruleGenStatusEl.className = 'rule-status success';
3551
+
3552
+ // Show preview of generated rules
3553
+ var iconMap = { block: '&#x1F534;', allow: '&#x1F7E2;', modify: '&#x1F535;', warn: '&#x1F7E1;', pause: '&#x1F7E1;' };
3554
+ generatedRulesPreviewEl.innerHTML = currentRules.map(function(r) {
3555
+ var icon = iconMap[r.enforcement] || '&#x1F7E2;';
3556
+ return '<div class="parsed-rule enforcement-' + r.enforcement + '">' +
3557
+ '<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + (r.enforcement || 'rule') + '</span></div>' +
3558
+ '<div class="pr-desc">' + r.description + '</div>' +
3559
+ '</div>';
3560
+ }).join('');
3561
+
3562
+ // Update active invariants display (hidden, for audit)
3563
+ activeInvEl.innerHTML = currentRules.map(function(r) {
3564
+ return '<div class="inv-item">[' + r.id + '] ' + r.description + '</div>';
3565
+ }).join('');
3566
+
3567
+ // If the generated world has state variables, show them
3568
+ if (data.world && data.world.stateVariables && data.world.stateVariables.length > 0) {
3569
+ currentWorld = {
3570
+ id: data.world.id || 'generated',
3571
+ title: data.world.title || 'Generated World',
3572
+ thesis: thesis,
3573
+ stateVariables: data.world.stateVariables,
3574
+ invariants: data.world.invariants || [],
3575
+ gates: data.world.gates || [],
3576
+ };
3577
+ renderStateVars();
3578
+ }
3579
+ }
3580
+ } catch (err) {
3581
+ ruleGenStatusEl.textContent = 'Error: ' + err.message;
3582
+ ruleGenStatusEl.className = 'rule-status error';
3583
+ }
3584
+
3585
+ generateRulesBtn.disabled = false;
3586
+ generateRulesBtn.textContent = 'Generate Governance Rules';
3587
+ });
2390
3588
 
2391
3589
  // ============================================
2392
- // WORLD SELECT
3590
+ // WORLD FILE TOGGLE
2393
3591
  // ============================================
2394
- worldSelect.addEventListener('change', () => selectWorld(worldSelect.value));
2395
- roundsSlider.addEventListener('input', () => { roundsVal.textContent = roundsSlider.value; });
3592
+ document.getElementById('worldfile-toggle').addEventListener('click', () => {
3593
+ const panel = document.getElementById('worldfile-panel');
3594
+ const arrow = document.querySelector('#worldfile-toggle .wf-arrow');
3595
+ if (panel.style.display === 'none') {
3596
+ panel.style.display = '';
3597
+ arrow.innerHTML = '&#x25BC;';
3598
+ } else {
3599
+ panel.style.display = 'none';
3600
+ arrow.innerHTML = '&#x25B6;';
3601
+ }
3602
+ });
2396
3603
 
2397
3604
  // ============================================
2398
- // RUN SIMULATION
3605
+ // BRIDGE CONNECTION
3606
+ // ============================================
3607
+ const bridgeButtons = document.querySelectorAll('.bridge-btn');
3608
+ const bridgeStatusEl = document.getElementById('bridge-status');
3609
+ const bridgeSetupEl = document.getElementById('bridge-setup');
3610
+ const bridgeSetupContentEl = document.getElementById('bridge-setup-content');
3611
+ const bridgeEndpointInput = document.getElementById('bridge-endpoint');
3612
+ const bridgeConnectBtn = document.getElementById('bridge-connect-btn');
3613
+ const bridgeControlsSection = document.getElementById('bridge-controls-section');
3614
+ const bridgeControlsEl = document.getElementById('bridge-controls');
3615
+
3616
+ // Track detected bridge binary info
3617
+ let detectedBridgeInfo = null; // { binaryPath, version, canLaunch }
3618
+
3619
+ bridgeButtons.forEach(btn => {
3620
+ btn.addEventListener('click', async () => {
3621
+ const bridgeId = btn.dataset.bridge;
3622
+ const caps = bridgeCapabilities[bridgeId];
3623
+
3624
+ // Highlight selected bridge
3625
+ bridgeButtons.forEach(b => b.classList.remove('active'));
3626
+ btn.classList.add('active');
3627
+
3628
+ if (bridgeId === 'scienceclaw') {
3629
+ // Auto-detect ScienceClaw — no manual setup needed
3630
+ bridgeSetupEl.style.display = '';
3631
+ bridgeEndpointInput.style.display = 'none';
3632
+ bridgeStatusEl.innerHTML = '<span style="color:var(--dim)">Detecting ScienceClaw...</span>';
3633
+ bridgeSetupContentEl.innerHTML = '';
3634
+ bridgeConnectBtn.disabled = true;
3635
+
3636
+ try {
3637
+ const resp = await fetch('/api/bridge-check', {
3638
+ method: 'POST',
3639
+ headers: { 'Content-Type': 'application/json' },
3640
+ body: JSON.stringify({ bridgeId: 'scienceclaw', endpoint: '' }),
3641
+ });
3642
+ const data = await resp.json();
3643
+ detectedBridgeInfo = data;
3644
+
3645
+ if (data.detected && data.canLaunch) {
3646
+ // ScienceClaw found on PATH — show launch UI
3647
+ bridgeStatusEl.innerHTML = '<span style="color:var(--green)">ScienceClaw detected</span>';
3648
+ bridgeSetupContentEl.innerHTML =
3649
+ '<div class="bridge-instructions">' +
3650
+ '<div style="color:var(--green);margin-bottom:8px">Binary: ' + data.binaryPath + '</div>' +
3651
+ (data.version ? '<div style="color:var(--dim);margin-bottom:12px">Version: ' + data.version + '</div>' : '') +
3652
+ '<div style="margin-bottom:8px">Governance server ready. Click <strong>Launch</strong> to run a governed ScienceClaw session.</div>' +
3653
+ '<div class="bridge-launch-row" style="margin-top:12px">' +
3654
+ '<input type="text" id="bridge-sc-args" class="bridge-endpoint-input" placeholder="--agent MyAgent --topic &quot;CRISPR risks&quot;" style="display:block">' +
3655
+ '</div>' +
3656
+ '</div>';
3657
+ bridgeConnectBtn.textContent = 'Launch Governed Session';
3658
+ bridgeConnectBtn.disabled = false;
3659
+ } else {
3660
+ // ScienceClaw not auto-detected — show manual connection options
3661
+ bridgeStatusEl.innerHTML = '<span style="color:var(--accent)">ScienceClaw not detected on PATH</span>';
3662
+ bridgeSetupContentEl.innerHTML =
3663
+ '<div class="bridge-instructions">' +
3664
+ '<div style="margin-bottom:8px">ScienceClaw runs independently — NeuroVerse connects and governs it live.</div>' +
3665
+ '<div style="margin-bottom:4px"><strong>Option 1:</strong> Install ScienceClaw on your PATH, then re-click to auto-detect and launch directly from here.</div>' +
3666
+ '<div style="margin-bottom:4px"><strong>Option 2:</strong> Run the governance wrapper in a separate terminal:</div>' +
3667
+ '<div style="font-family:monospace;background:rgba(255,255,255,0.05);padding:8px;border-radius:4px;margin:8px 0">' +
3668
+ 'python connectors/nv_scienceclaw_wrapper.py --agent MyAgent --topic "..."' +
3669
+ '</div>' +
3670
+ '<div style="color:var(--dim);margin-bottom:8px;font-size:0.85em">The wrapper launches ScienceClaw and streams each artifact cycle through governance in real time.</div>' +
3671
+ '<div style="margin-bottom:4px"><strong>Option 3:</strong> Point any running ScienceClaw instance at the governance endpoint:</div>' +
3672
+ '<div style="font-family:monospace;background:rgba(255,255,255,0.05);padding:8px;border-radius:4px;margin:8px 0">' +
3673
+ 'POST /api/evaluate { actor, action, payload }' +
3674
+ '</div>' +
3675
+ '<div style="color:var(--dim);font-size:0.85em">Once ScienceClaw is running, click Connect to begin governed evaluation.</div>' +
3676
+ '</div>';
3677
+ bridgeConnectBtn.textContent = 'Connect';
3678
+ bridgeConnectBtn.disabled = false;
3679
+ }
3680
+ } catch (err) {
3681
+ bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Detection failed: ' + err.message + '</span>';
3682
+ bridgeConnectBtn.disabled = false;
3683
+ }
3684
+ } else if (caps) {
3685
+ // Other bridges — show standard setup
3686
+ bridgeSetupEl.style.display = '';
3687
+ bridgeSetupContentEl.innerHTML = '<div class="bridge-instructions">' +
3688
+ caps.setupInstructions.replace(/\\n/g, '<br>') +
3689
+ '</div>';
3690
+ bridgeEndpointInput.value = 'http://localhost:5001';
3691
+ bridgeEndpointInput.style.display = '';
3692
+ bridgeConnectBtn.textContent = 'Connect';
3693
+ bridgeConnectBtn.disabled = false;
3694
+ bridgeStatusEl.innerHTML = '';
3695
+ }
3696
+ });
3697
+ });
3698
+
3699
+ bridgeConnectBtn.addEventListener('click', async () => {
3700
+ const activeBridge = document.querySelector('.bridge-btn.active');
3701
+ if (!activeBridge) return;
3702
+ const bridgeId = activeBridge.dataset.bridge;
3703
+ const endpoint = bridgeEndpointInput.value.trim();
3704
+
3705
+ bridgeConnectBtn.disabled = true;
3706
+
3707
+ if (bridgeId === 'scienceclaw' && detectedBridgeInfo && detectedBridgeInfo.canLaunch) {
3708
+ // ── LAUNCH governed ScienceClaw session ──
3709
+ bridgeConnectBtn.textContent = 'Launching...';
3710
+ bridgeStatusEl.innerHTML = '<span style="color:var(--dim)">Starting ScienceClaw with governance...</span>';
3711
+
3712
+ const scArgsInput = document.getElementById('bridge-sc-args');
3713
+ const scArgs = scArgsInput ? scArgsInput.value.trim().split(/\\s+/).filter(Boolean) : [];
3714
+
3715
+ try {
3716
+ const resp = await fetch('/api/bridge-launch', {
3717
+ method: 'POST',
3718
+ headers: { 'Content-Type': 'application/json' },
3719
+ body: JSON.stringify({
3720
+ bridgeId: 'scienceclaw',
3721
+ args: scArgs.length > 0 ? scArgs : undefined,
3722
+ binaryPath: detectedBridgeInfo.binaryPath,
3723
+ }),
3724
+ });
3725
+ const data = await resp.json();
3726
+
3727
+ if (data.launched) {
3728
+ connectedBridge = {
3729
+ id: bridgeId,
3730
+ label: 'ScienceClaw',
3731
+ capabilities: bridgeCapabilities[bridgeId],
3732
+ endpoint: 'local (PID ' + data.pid + ')',
3733
+ };
3734
+ bridgeStatusEl.innerHTML =
3735
+ '<span style="color:var(--green)">ScienceClaw running (PID ' + data.pid + ')</span>' +
3736
+ '<br><span style="color:var(--dim);font-size:11px">Governance: ' + data.launchMethod + ' | Output streaming to dashboard</span>';
3737
+ bridgeSetupEl.style.display = 'none';
3738
+
3739
+ // Change connect button to stop button
3740
+ bridgeConnectBtn.textContent = 'Stop';
3741
+ bridgeConnectBtn.disabled = false;
3742
+ bridgeConnectBtn.onclick = async () => {
3743
+ await fetch('/api/bridge-stop', { method: 'POST' });
3744
+ bridgeStatusEl.innerHTML = '<span style="color:var(--dim)">ScienceClaw stopped</span>';
3745
+ bridgeConnectBtn.textContent = 'Launch Governed Session';
3746
+ bridgeConnectBtn.onclick = null; // reset to default handler
3747
+ };
3748
+
3749
+ showBridgeControls(bridgeId);
3750
+ } else {
3751
+ bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Launch failed' + (data.error ? ': ' + data.error : '') + '</span>';
3752
+ }
3753
+ } catch (err) {
3754
+ bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Launch error: ' + err.message + '</span>';
3755
+ }
3756
+
3757
+ bridgeConnectBtn.disabled = false;
3758
+ if (bridgeConnectBtn.textContent === 'Launching...') bridgeConnectBtn.textContent = 'Launch Governed Session';
3759
+ return;
3760
+ }
3761
+
3762
+ // ── Standard connect flow (ScienceClaw without auto-detect, or other bridges) ──
3763
+ bridgeConnectBtn.textContent = 'Connecting...';
3764
+ bridgeStatusEl.innerHTML = '';
3765
+
3766
+ try {
3767
+ const resp = await fetch('/api/bridge-check', {
3768
+ method: 'POST',
3769
+ headers: { 'Content-Type': 'application/json' },
3770
+ body: JSON.stringify({ bridgeId, endpoint }),
3771
+ });
3772
+ const data = await resp.json();
3773
+
3774
+ if (data.connected) {
3775
+ connectedBridge = {
3776
+ id: bridgeId,
3777
+ label: bridgeCapabilities[bridgeId]?.label || bridgeId,
3778
+ capabilities: bridgeCapabilities[bridgeId],
3779
+ endpoint: endpoint || 'local',
3780
+ };
3781
+ bridgeStatusEl.innerHTML = '<span style="color:var(--green)">Connected to ' + connectedBridge.label + '</span>';
3782
+ bridgeSetupEl.style.display = 'none';
3783
+
3784
+ showBridgeControls(bridgeId);
3785
+ } else {
3786
+ bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Connection failed' + (data.error ? ': ' + data.error : '') + '</span>';
3787
+ }
3788
+ } catch (err) {
3789
+ bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Error: ' + err.message + '</span>';
3790
+ }
3791
+
3792
+ bridgeConnectBtn.disabled = false;
3793
+ bridgeConnectBtn.textContent = 'Connect';
3794
+ });
3795
+
3796
+ function showBridgeControls(bridgeId) {
3797
+ const caps = bridgeCapabilities[bridgeId];
3798
+ if (!caps) return;
3799
+
3800
+ bridgeControlsSection.style.display = '';
3801
+ document.getElementById('bridge-controls-label').textContent = caps.label + ' Settings';
3802
+
3803
+ var html = '';
3804
+ if (caps.governanceTiming === 'live') {
3805
+ html += '<div class="bridge-info-row"><span class="bridge-info-icon" style="color:var(--green)">&#x25CF;</span> Live governance — interventions happen during simulation</div>';
3806
+ } else {
3807
+ html += '<div class="bridge-info-row"><span class="bridge-info-icon" style="color:var(--accent)">&#x25CF;</span> Post-hoc governance — evaluation after each batch</div>';
3808
+ }
3809
+ if (caps.canHalt) {
3810
+ html += '<div class="bridge-info-row"><span class="bridge-info-icon">&#x23F9;</span> Can halt simulation if safety thresholds are exceeded</div>';
3811
+ }
3812
+ if (bridgeId === 'scienceclaw') {
3813
+ html += '<div class="bridge-info-row"><span class="bridge-info-icon" style="color:var(--green)">&#x25CF;</span> REWARD/PENALIZE incentive system active</div>';
3814
+ }
3815
+
3816
+ bridgeControlsEl.innerHTML = html;
3817
+ }
3818
+
3819
+ // ============================================
3820
+ // RUN GOVERNANCE
2399
3821
  // ============================================
2400
3822
  runBtn.addEventListener('click', async () => {
2401
- if (!currentWorld) return;
3823
+ if (!connectedBridge && currentRules.length === 0) {
3824
+ ruleGenStatusEl.textContent = 'Generate governance rules or connect to a simulator first.';
3825
+ ruleGenStatusEl.className = 'rule-status error';
3826
+ return;
3827
+ }
3828
+
2402
3829
  runBtn.disabled = true;
2403
3830
  runBtn.textContent = 'Running...';
2404
3831
 
2405
3832
  // Reset viewer state
2406
3833
  totalInterventions = 0;
2407
- baselineImpacts = [];
2408
- governedImpacts = [];
2409
- chartLabels = [];
2410
- if (chart) { chart.destroy(); chart = null; }
3834
+ totalActions = 0;
2411
3835
  agentsEl.innerHTML = '';
2412
3836
  logEl.innerHTML = '';
2413
3837
  document.getElementById('m-stability').textContent = '--';
@@ -2415,9 +3839,9 @@ runBtn.addEventListener('click', async () => {
2415
3839
  document.getElementById('m-round').textContent = '--';
2416
3840
  document.getElementById('m-interventions').textContent = '0';
2417
3841
 
2418
- // Gather state overrides
3842
+ // Gather state overrides from sliders (if world has them)
2419
3843
  const stateOverrides = {};
2420
- if (currentWorld.stateVariables) {
3844
+ if (currentWorld && currentWorld.stateVariables) {
2421
3845
  currentWorld.stateVariables.forEach(sv => {
2422
3846
  const el = document.getElementById('sv-' + sv.id);
2423
3847
  if (!el) return;
@@ -2427,39 +3851,36 @@ runBtn.addEventListener('click', async () => {
2427
3851
  });
2428
3852
  }
2429
3853
 
2430
- const selectedEngine = engineSelect.value;
2431
-
2432
3854
  try {
2433
- if (selectedEngine === 'nv-sim') {
2434
- // Built-in simulation
2435
- const config = {
2436
- worldId: currentWorld.id,
3855
+ if (connectedBridge) {
3856
+ // Run via bridge adapter
3857
+ const payload = {
3858
+ adapterId: connectedBridge.id,
3859
+ worldId: currentWorld ? currentWorld.id : 'social_simulation',
2437
3860
  stateOverrides,
2438
- injectEvents: injectedEvents.length > 0 ? injectedEvents : undefined,
2439
- rounds: parseInt(roundsSlider.value),
2440
3861
  };
2441
- await fetch('/api/run-sim', {
3862
+ await fetch('/api/run-live', {
2442
3863
  method: 'POST',
2443
3864
  headers: { 'Content-Type': 'application/json' },
2444
- body: JSON.stringify(config),
3865
+ body: JSON.stringify(payload),
2445
3866
  });
2446
3867
  } else {
2447
- // Live adapter (external simulator)
2448
- const payload = {
2449
- adapterId: selectedEngine,
2450
- worldId: currentWorld.id,
3868
+ // Fallback: run internal simulation with generated rules
3869
+ const config = {
3870
+ worldId: currentWorld ? currentWorld.id : 'social_simulation',
2451
3871
  stateOverrides,
3872
+ rounds: 5,
2452
3873
  };
2453
- await fetch('/api/run-live', {
3874
+ await fetch('/api/run-sim', {
2454
3875
  method: 'POST',
2455
3876
  headers: { 'Content-Type': 'application/json' },
2456
- body: JSON.stringify(payload),
3877
+ body: JSON.stringify(config),
2457
3878
  });
2458
3879
  }
2459
3880
  } catch (err) {
2460
- addLog('Error starting simulation: ' + err.message, 'block');
3881
+ addLog('Error starting governance: ' + err.message, 'block');
2461
3882
  runBtn.disabled = false;
2462
- runBtn.textContent = 'Run Simulation';
3883
+ runBtn.textContent = 'Run Governance';
2463
3884
  }
2464
3885
  });
2465
3886
 
@@ -2475,28 +3896,7 @@ function connectSSE() {
2475
3896
  }
2476
3897
 
2477
3898
  function initChart() {
2478
- if (typeof Chart === 'undefined') return;
2479
- const ctx = document.getElementById('chart');
2480
- chart = new Chart(ctx, {
2481
- type: 'line',
2482
- data: {
2483
- labels: chartLabels,
2484
- datasets: [
2485
- { label: 'Baseline', data: baselineImpacts, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
2486
- { label: 'Governed', data: governedImpacts, borderColor: '#4ade80', backgroundColor: 'rgba(74,222,128,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
2487
- ]
2488
- },
2489
- options: {
2490
- animation: { duration: 400 },
2491
- responsive: true,
2492
- maintainAspectRatio: false,
2493
- plugins: { legend: { labels: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888', font: { family: 'monospace', size: 10 } } } },
2494
- scales: {
2495
- x: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' } },
2496
- y: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' }, min: -1, max: 1 }
2497
- }
2498
- }
2499
- });
3899
+ // Chart removed no longer needed in outcome-first view
2500
3900
  }
2501
3901
 
2502
3902
  function addLog(msg, cls) {
@@ -2607,23 +4007,342 @@ function renderAgents(reactions) {
2607
4007
  agentsEl.innerHTML = html;
2608
4008
  }
2609
4009
 
4010
+ // ============================================
4011
+ // OBSERVATION DECK — Rendering
4012
+ // ============================================
4013
+
4014
+ // Per-round snapshots for behavioral drift
4015
+ var roundSnapshots = []; // [{ round, choices: { action: count }, flows: [{ from, to, count, status }] }]
4016
+ var firstRoundSnapshot = null;
4017
+
4018
+ function recordBehavior(reactions, round) {
4019
+ if (!reactions || !reactions.length) return;
4020
+ reactions.forEach(function(r) {
4021
+ var status = r.verdict ? r.verdict.status : 'ALLOW';
4022
+ var reason = r.verdict ? (r.verdict.reason || '') : '';
4023
+ behaviorLog.push({
4024
+ agent: r.stakeholder_id,
4025
+ action: r.reaction || 'acted',
4026
+ status: status,
4027
+ reason: reason,
4028
+ round: round || 0,
4029
+ ts: Date.now(),
4030
+ });
4031
+ });
4032
+ }
4033
+
4034
+ // Normalize action names into readable categories
4035
+ function normalizeAction(action) {
4036
+ if (!action) return 'act';
4037
+ var a = action.toLowerCase().replace(/_/g, ' ');
4038
+ // Collapse similar actions
4039
+ if (a.includes('panic sell') || a.includes('panic_sell')) return 'panic sell';
4040
+ if (a.includes('reduce') || a.includes('de-risk')) return 'reduce exposure';
4041
+ if (a.includes('hold') || a.includes('maintain') || a.includes('steady')) return 'hold steady';
4042
+ if (a.includes('aggressive') && a.includes('buy')) return 'aggressive buy';
4043
+ if (a.includes('buy') || a.includes('accumulate')) return 'buy';
4044
+ if (a.includes('sell') || a.includes('liquidat')) return 'sell';
4045
+ if (a.includes('diversif')) return 'diversify';
4046
+ if (a.includes('hedge')) return 'hedge';
4047
+ if (a.includes('wait') || a.includes('observe') || a.includes('monitor')) return 'wait & observe';
4048
+ if (a.includes('short')) return 'short';
4049
+ if (a.includes('post') || a.includes('create')) return 'create content';
4050
+ if (a.includes('follow') || a.includes('like') || a.includes('repost')) return 'engage';
4051
+ if (a.includes('publish') || a.includes('cite')) return 'publish';
4052
+ if (a.includes('analyze') || a.includes('research')) return 'research';
4053
+ // Return first 20 chars
4054
+ return a.slice(0, 20);
4055
+ }
4056
+
4057
+ // Color for an action category
4058
+ function actionColor(action) {
4059
+ var colors = {
4060
+ 'panic sell': '#ef4444', 'sell': '#f87171', 'short': '#dc2626',
4061
+ 'aggressive buy': '#f59e0b', 'buy': '#22c55e', 'accumulate': '#22c55e',
4062
+ 'hold steady': '#3b82f6', 'wait & observe': '#6366f1',
4063
+ 'reduce exposure': '#a78bfa', 'diversify': '#8b5cf6', 'hedge': '#7c3aed',
4064
+ 'create content': '#06b6d4', 'engage': '#14b8a6', 'publish': '#0ea5e9', 'research': '#818cf8',
4065
+ };
4066
+ return colors[action] || '#64748b';
4067
+ }
4068
+
4069
+ // ── Choice Bubbles ──
4070
+ function renderChoiceBubbles(reactions) {
4071
+ var landscapeEl = document.getElementById('choice-landscape');
4072
+ if (!landscapeEl) return;
4073
+
4074
+ // Group by normalized action
4075
+ var groups = {};
4076
+ reactions.forEach(function(r) {
4077
+ var action = normalizeAction(r.reaction);
4078
+ if (!groups[action]) groups[action] = { count: 0, totalImpact: 0 };
4079
+ groups[action].count++;
4080
+ groups[action].totalImpact += r.impact || 0;
4081
+ });
4082
+
4083
+ var total = reactions.length;
4084
+ var entries = Object.entries(groups).sort(function(a, b) { return b[1].count - a[1].count; });
4085
+
4086
+ // Hide empty state
4087
+ var emptyEl = document.getElementById('choice-empty');
4088
+ if (emptyEl) emptyEl.style.display = 'none';
4089
+
4090
+ var html = entries.map(function(entry) {
4091
+ var action = entry[0];
4092
+ var data = entry[1];
4093
+ var pct = Math.round(data.count / total * 100);
4094
+ // Size: min 48px, max 120px, proportional to count
4095
+ var size = Math.max(48, Math.min(120, 48 + (data.count / total) * 100));
4096
+ var color = actionColor(action);
4097
+ var fontSize = size > 70 ? 18 : 14;
4098
+ return '<div class="choice-bubble" style="width:' + size + 'px;height:' + size + 'px;background:' + color + '" title="' + data.count + ' agents chose ' + action + '">' +
4099
+ '<div class="cb-count" style="font-size:' + fontSize + 'px">' + data.count + '</div>' +
4100
+ '<div class="cb-label">' + action + '</div>' +
4101
+ '<div class="cb-pct">' + pct + '%</div>' +
4102
+ '</div>';
4103
+ }).join('');
4104
+
4105
+ landscapeEl.innerHTML = html;
4106
+ }
4107
+
4108
+ // ── Decision Flow (rules in center, agents flowing around) ──
4109
+ function renderDecisionFlow(reactions) {
4110
+ var landscapeEl = document.getElementById('choice-landscape');
4111
+ if (!landscapeEl) return;
4112
+
4113
+ // Build flow data: what agents intended → what happened
4114
+ var flows = {}; // key: "intended|status|actual" → count
4115
+ var intendedCounts = {};
4116
+ var actualCounts = {};
4117
+
4118
+ reactions.forEach(function(r) {
4119
+ var intended = normalizeAction(r.reaction);
4120
+ var status = r.verdict ? r.verdict.status : 'ALLOW';
4121
+ var actual = intended; // for ALLOW, actual = intended
4122
+ if (status === 'BLOCK') actual = '(blocked)';
4123
+ else if (status === 'MODIFY') actual = normalizeAction(r.verdict && r.verdict.modified_action ? r.verdict.modified_action : r.reaction) + ' (adj)';
4124
+ else if (status === 'PENALIZE') actual = intended + ' (penalized)';
4125
+ else if (status === 'REWARD') actual = intended + ' (rewarded)';
4126
+
4127
+ var key = intended + '|' + status + '|' + actual;
4128
+ flows[key] = (flows[key] || 0) + 1;
4129
+ intendedCounts[intended] = (intendedCounts[intended] || 0) + 1;
4130
+ actualCounts[actual] = (actualCounts[actual] || 0) + 1;
4131
+ });
4132
+
4133
+ // Sort by count
4134
+ var sortedFlows = Object.entries(flows).sort(function(a, b) { return b[1] - a[1]; }).slice(0, 12);
4135
+ var total = reactions.length;
4136
+ var hasGovernance = sortedFlows.some(function(f) { return f[0].indexOf('BLOCK') >= 0 || f[0].indexOf('MODIFY') >= 0 || f[0].indexOf('REWARD') >= 0 || f[0].indexOf('PENALIZE') >= 0; });
4137
+
4138
+ if (!hasGovernance) {
4139
+ // No governance interventions — just show bubbles
4140
+ renderChoiceBubbles(reactions);
4141
+ return;
4142
+ }
4143
+
4144
+ // Hide empty state
4145
+ var emptyEl = document.getElementById('choice-empty');
4146
+ if (emptyEl) emptyEl.style.display = 'none';
4147
+
4148
+ // Build flow rows
4149
+ var maxCount = sortedFlows.length > 0 ? sortedFlows[0][1] : 1;
4150
+ var html = '<div class="decision-flow">';
4151
+
4152
+ sortedFlows.forEach(function(entry) {
4153
+ var parts = entry[0].split('|');
4154
+ var intended = parts[0], status = parts[1], actual = parts[2];
4155
+ var count = entry[1];
4156
+ var barWidth = Math.max(20, (count / maxCount) * 100);
4157
+ var arrowCls = status === 'BLOCK' ? 'blocked' : status === 'PENALIZE' ? 'penalized' : status === 'MODIFY' ? 'modified' : status === 'REWARD' ? 'rewarded' : 'allowed';
4158
+ var arrowChar = status === 'BLOCK' ? '&#x2716;' : status === 'PENALIZE' ? '&#x26A0;' : status === 'MODIFY' ? '&#x2794;' : status === 'REWARD' ? '&#x2605;' : '&#x2192;';
4159
+ var actualCls = 'actual-' + (status === 'BLOCK' ? 'block' : status === 'PENALIZE' ? 'penalize' : status === 'MODIFY' ? 'modify' : status === 'REWARD' ? 'reward' : 'allow');
4160
+
4161
+ html += '<div class="flow-row">' +
4162
+ '<div class="flow-intended"><span class="flow-label">' + intended + '</span><div class="flow-bar intended" style="width:' + barWidth + '%"><span class="flow-count">' + count + '</span></div></div>' +
4163
+ '<div class="flow-arrow ' + arrowCls + '">' + arrowChar + '</div>' +
4164
+ '<div class="flow-actual"><div class="flow-bar ' + actualCls + '" style="width:' + barWidth + '%"><span class="flow-count">' + count + '</span></div><span class="flow-label">' + actual + '</span></div>' +
4165
+ '</div>';
4166
+ });
4167
+
4168
+ html += '<div class="flow-legend">' +
4169
+ '<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#4ade80"></span> proceeded</div>' +
4170
+ '<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#fbbf24"></span> adjusted</div>' +
4171
+ '<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#f87171"></span> stopped</div>' +
4172
+ '<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#4ade80;border:1px solid #22c55e"></span> rewarded</div>' +
4173
+ '<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#fb923c"></span> penalized</div>' +
4174
+ '</div>';
4175
+ html += '</div>';
4176
+
4177
+ landscapeEl.innerHTML = html;
4178
+ }
4179
+
4180
+ // ── Choice Stream ──
4181
+ function updateChoiceStream(reactions, round) {
4182
+ var streamEl = document.getElementById('choice-stream');
4183
+ var countEl = document.getElementById('stream-count');
4184
+ if (!streamEl) return;
4185
+
4186
+ // Take latest 8 reactions to show
4187
+ var recent = reactions.slice(-8).reverse();
4188
+ var html = recent.map(function(r) {
4189
+ var status = r.verdict ? r.verdict.status : 'ALLOW';
4190
+ var badge = '';
4191
+ if (status === 'PENALIZE') badge = '<span class="incentive-badge penalize">&#x26A0;</span> ';
4192
+ else if (status === 'REWARD') badge = '<span class="incentive-badge reward">&#x2605;</span> ';
4193
+ else if (status === 'BLOCK') badge = '<span style="color:#f87171;font-size:9px">&#x2716;</span> ';
4194
+ var frozenInfo = r.verdict && r.verdict.agentState && r.verdict.agentState.cooldown > 0
4195
+ ? ' <span style="color:#818cf8;font-size:9px">(frozen)</span>' : '';
4196
+ return '<div class="stream-item">' +
4197
+ badge +
4198
+ '<span class="stream-agent">' + r.stakeholder_id + '</span>' +
4199
+ '<span class="stream-action">chose to <strong>' + normalizeAction(r.reaction) + '</strong>' + frozenInfo + '</span>' +
4200
+ '<span class="stream-round">R' + round + '</span>' +
4201
+ '</div>';
4202
+ }).join('');
4203
+
4204
+ streamEl.innerHTML = html;
4205
+ if (countEl) countEl.textContent = totalActions + ' total';
4206
+ }
4207
+
4208
+ // ── Snapshot for drift ──
4209
+ function takeRoundSnapshot(reactions, round) {
4210
+ var choices = {};
4211
+ reactions.forEach(function(r) {
4212
+ var action = normalizeAction(r.reaction);
4213
+ choices[action] = (choices[action] || 0) + 1;
4214
+ });
4215
+
4216
+ var snapshot = { round: round, choices: choices, total: reactions.length };
4217
+ roundSnapshots.push(snapshot);
4218
+ if (!firstRoundSnapshot) firstRoundSnapshot = snapshot;
4219
+ }
4220
+
4221
+ // ── Behavioral Drift (post-run) ──
4222
+ function renderBehavioralDrift() {
4223
+ var driftSection = document.getElementById('deck-drift-section');
4224
+ var verdictEl = document.getElementById('drift-verdict');
4225
+ var comparisonEl = document.getElementById('drift-comparison');
4226
+ if (!driftSection || !verdictEl || !comparisonEl) return;
4227
+ if (roundSnapshots.length < 2) return;
4228
+
4229
+ var first = firstRoundSnapshot;
4230
+ var last = roundSnapshots[roundSnapshots.length - 1];
4231
+ driftSection.style.display = '';
4232
+
4233
+ // Collect all actions across both snapshots
4234
+ var allActions = {};
4235
+ Object.keys(first.choices).forEach(function(a) { allActions[a] = true; });
4236
+ Object.keys(last.choices).forEach(function(a) { allActions[a] = true; });
4237
+ var actions = Object.keys(allActions).sort(function(a, b) {
4238
+ return (last.choices[b] || 0) - (last.choices[a] || 0);
4239
+ });
4240
+
4241
+ // Generate verdict
4242
+ var biggestGain = { action: '', delta: 0 };
4243
+ var biggestLoss = { action: '', delta: 0 };
4244
+ actions.forEach(function(a) {
4245
+ var earlyPct = first.total > 0 ? (first.choices[a] || 0) / first.total : 0;
4246
+ var latePct = last.total > 0 ? (last.choices[a] || 0) / last.total : 0;
4247
+ var delta = latePct - earlyPct;
4248
+ if (delta > biggestGain.delta) { biggestGain = { action: a, delta: delta }; }
4249
+ if (delta < biggestLoss.delta) { biggestLoss = { action: a, delta: delta }; }
4250
+ });
4251
+
4252
+ var verdictParts = [];
4253
+ if (biggestGain.action && biggestGain.delta > 0.05) {
4254
+ verdictParts.push('Agents shifted toward <strong>' + biggestGain.action + '</strong> (+' + Math.round(biggestGain.delta * 100) + '%)');
4255
+ }
4256
+ if (biggestLoss.action && biggestLoss.delta < -0.05) {
4257
+ verdictParts.push('abandoned <strong>' + biggestLoss.action + '</strong> (' + Math.round(biggestLoss.delta * 100) + '%)');
4258
+ }
4259
+ verdictEl.innerHTML = verdictParts.length > 0
4260
+ ? verdictParts.join(', ')
4261
+ : 'Agent behavior remained relatively stable across rounds.';
4262
+
4263
+ // Render drift bars
4264
+ var html = '<div class="drift-header-row"><span>Choice</span><span>Round 1</span><span></span><span>Final Round</span></div>';
4265
+ actions.slice(0, 8).forEach(function(action) {
4266
+ var earlyCount = first.choices[action] || 0;
4267
+ var lateCount = last.choices[action] || 0;
4268
+ var earlyPct = first.total > 0 ? Math.round(earlyCount / first.total * 100) : 0;
4269
+ var latePct = last.total > 0 ? Math.round(lateCount / last.total * 100) : 0;
4270
+ var delta = latePct - earlyPct;
4271
+ var deltaCls = delta > 2 ? 'up' : delta < -2 ? 'down' : 'flat';
4272
+ var deltaStr = delta > 0 ? '+' + delta + '%' : delta + '%';
4273
+ if (Math.abs(delta) <= 2) deltaStr = '—';
4274
+ var color = actionColor(action);
4275
+
4276
+ html += '<div class="drift-row">' +
4277
+ '<div class="drift-action">' + action + '</div>' +
4278
+ '<div class="drift-bar-container"><div class="drift-bar early" style="width:' + Math.max(2, earlyPct) + '%;background:' + color + ';opacity:0.5">' + earlyPct + '%</div></div>' +
4279
+ '<div class="drift-delta ' + deltaCls + '">' + deltaStr + '</div>' +
4280
+ '<div class="drift-bar-container"><div class="drift-bar late" style="width:' + Math.max(2, latePct) + '%;background:' + color + '">' + latePct + '%</div></div>' +
4281
+ '</div>';
4282
+ });
4283
+
4284
+ comparisonEl.innerHTML = html;
4285
+ }
4286
+
4287
+ // ── What-If counterfactual ──
4288
+ function renderWhatIf(result) {
4289
+ var section = document.getElementById('deck-whatif-section');
4290
+ var contentEl = document.getElementById('whatif-content');
4291
+ if (!section || !contentEl || !result || !result.governed || !result.baseline) return;
4292
+
4293
+ section.style.display = '';
4294
+ var govCollapse = Math.round(result.governed.metrics.collapseProbability * 100);
4295
+ var baseCollapse = Math.round(result.baseline.metrics.collapseProbability * 100);
4296
+ var govStability = Math.round(result.governed.metrics.stabilityScore * 100);
4297
+ var baseStability = Math.round(result.baseline.metrics.stabilityScore * 100);
4298
+
4299
+ contentEl.innerHTML =
4300
+ '<div class="whatif-stat"><span class="whatif-num bad">' + baseCollapse + '%</span><span class="whatif-label">collapse probability <em>without</em> your rules</span></div>' +
4301
+ '<div class="whatif-stat"><span class="whatif-num good">' + govCollapse + '%</span><span class="whatif-label">collapse probability <em>with</em> your rules</span></div>' +
4302
+ '<div class="whatif-compare">Stability went from ' + baseStability + '% to ' + govStability + '%. ' +
4303
+ (govStability > baseStability ? 'Your rules made the system ' + (govStability - baseStability) + ' points more stable.' : 'Agents found alternative strategies within your constraints.') +
4304
+ '</div>';
4305
+ }
4306
+
4307
+ // ── Master update ──
4308
+ function updateAllPanels() {
4309
+ // Observation Deck updates are driven by renderChoiceBubbles/renderDecisionFlow
4310
+ // called directly from handleEvent — this function exists for compatibility
4311
+ }
4312
+
2610
4313
  function handleEvent(event) {
2611
4314
  if (event.type === 'meta') {
2612
4315
  statusEl.className = 'status live';
2613
4316
  statusEl.textContent = 'LIVE';
2614
4317
  // Show simulation source
2615
- const src = event.source || 'nv-sim';
2616
- if (src !== 'nv-sim') {
4318
+ const src = event.source || 'governance';
4319
+ if (src !== 'nv-sim' && src !== 'governance') {
2617
4320
  traceSourceEl.textContent = '● ' + src.toUpperCase() + ' (LIVE)';
2618
4321
  traceSourceEl.style.color = '#4ade80';
2619
- engineStatusEl.textContent = 'Streaming from ' + src;
2620
- engineStatusEl.style.color = '#4ade80';
2621
4322
  } else {
2622
4323
  traceSourceEl.textContent = '';
2623
- engineStatusEl.textContent = '';
2624
4324
  }
2625
4325
  addLog('Simulation started: ' + event.agents.length + ' agents, ' + event.totalRounds + ' rounds' + (src !== 'nv-sim' ? ' [source: ' + src + ']' : ''));
2626
4326
  resetShiftTracker();
4327
+ // Reset Observation Deck state
4328
+ roundSnapshots = [];
4329
+ firstRoundSnapshot = null;
4330
+ var deckStatusEl = document.getElementById('deck-status');
4331
+ if (deckStatusEl) { deckStatusEl.textContent = 'LIVE'; deckStatusEl.className = 'deck-status live'; }
4332
+ var driftSec = document.getElementById('deck-drift-section');
4333
+ if (driftSec) driftSec.style.display = 'none';
4334
+ var whatifSec = document.getElementById('deck-whatif-section');
4335
+ if (whatifSec) whatifSec.style.display = 'none';
4336
+ var choiceEmpty = document.getElementById('choice-empty');
4337
+ if (choiceEmpty) choiceEmpty.style.display = '';
4338
+ var roundLabel = document.getElementById('choice-round-label');
4339
+ if (roundLabel) roundLabel.style.display = 'none';
4340
+ var streamEl = document.getElementById('choice-stream');
4341
+ if (streamEl) streamEl.innerHTML = '<div class="stream-empty">Agent decisions will stream here in real time</div>';
4342
+ // Reset incentive tracker
4343
+ incentiveLog = [];
4344
+ var incentiveSec = document.getElementById('deck-incentive-section');
4345
+ if (incentiveSec) incentiveSec.style.display = 'none';
2627
4346
  // Store narrative events by round for trace rendering
2628
4347
  narrativeEventsByRound = {};
2629
4348
  (event.narrativeEvents || []).forEach(function(ev) {
@@ -2689,44 +4408,78 @@ function handleEvent(event) {
2689
4408
  '<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
2690
4409
  '</div>';
2691
4410
  }).join('');
2692
- initChart();
2693
4411
  }
2694
4412
 
2695
4413
  if (event.type === 'round') {
2696
- if (event.phase === 'baseline') {
2697
- chartLabels.push('R' + event.round);
2698
- baselineImpacts.push(event.avgImpact);
2699
- } else {
2700
- governedImpacts.push(event.avgImpact);
2701
- }
2702
- if (chart) chart.update();
4414
+ const isBridge = event.reactions && event.reactions[0] && event.reactions[0].trigger === 'bridge';
2703
4415
 
2704
- renderAgents(event.reactions);
2705
- document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
2706
- document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
2707
- document.getElementById('m-volatility').parentElement.className = 'metric-box ' + (event.maxVolatility > 0.6 ? 'bad' : event.maxVolatility > 0.4 ? 'warn' : 'good');
4416
+ // Record behavior data (no governance display)
4417
+ recordBehavior(event.reactions, event.round);
4418
+ renderAgents(event.reactions); // hidden, kept for audit
2708
4419
 
4420
+ // Track totals
4421
+ totalActions += event.reactions ? event.reactions.length : 0;
2709
4422
  totalInterventions += event.interventionCount;
4423
+ latestVolatility = event.maxVolatility || 0;
4424
+
4425
+ // Update hidden data elements for audit
4426
+ if (isBridge) {
4427
+ document.getElementById('m-round').textContent = event.round + ' evals';
4428
+ } else {
4429
+ document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
4430
+ }
4431
+ document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
2710
4432
  document.getElementById('m-interventions').textContent = totalInterventions;
2711
4433
 
2712
- // Track system shifts for the card
4434
+ // Track system shifts (data collection)
2713
4435
  trackShift(event);
2714
4436
 
2715
- // Render structured trace entry instead of flat log line
4437
+ // ── Observation Deck: live updates ──
4438
+ // Choice bubbles or decision flow (shows what agents chose and how it flowed)
4439
+ if (event.reactions && event.reactions.length > 0) {
4440
+ renderDecisionFlow(event.reactions);
4441
+ updateChoiceStream(event.reactions, event.round);
4442
+ takeRoundSnapshot(event.reactions, event.round);
4443
+
4444
+ // Track incentives (reward/penalize) from governance engine
4445
+ event.reactions.forEach(function(r) { trackIncentive(r, event.round); });
4446
+
4447
+ // Update round label
4448
+ var roundLabelEl = document.getElementById('choice-round-label');
4449
+ var roundNumEl = document.getElementById('choice-round-num');
4450
+ if (roundLabelEl) roundLabelEl.style.display = '';
4451
+ if (roundNumEl) roundNumEl.textContent = event.round;
4452
+ }
4453
+
4454
+ // Add trace entry for audit
2716
4455
  addTraceRound(event);
2717
4456
  }
2718
4457
 
4458
+ // Bridge metrics — updates stability from external /api/evaluate calls
4459
+ if (event.type === 'bridge_metrics') {
4460
+ latestStability = event.stability;
4461
+ document.getElementById('m-stability').textContent = (event.stability * 100).toFixed(0) + '%';
4462
+ updateAllPanels();
4463
+ }
4464
+
2719
4465
  if (event.type === 'complete') {
2720
4466
  statusEl.className = 'status complete';
2721
4467
  statusEl.textContent = 'COMPLETE';
2722
4468
  const r = event.result;
2723
4469
  if (r.governed) {
4470
+ latestStability = r.governed.metrics.stabilityScore;
4471
+ latestVolatility = r.governed.metrics.maxVolatility || latestVolatility;
2724
4472
  document.getElementById('m-stability').textContent = (r.governed.metrics.stabilityScore * 100).toFixed(0) + '%';
2725
- document.getElementById('m-stability').parentElement.className = 'metric-box ' + (r.governed.metrics.stabilityScore > 0.7 ? 'good' : 'warn');
2726
- addLog('Complete. Governance effectiveness: ' + (r.comparison.governanceEffectiveness * 100).toFixed(0) + '%');
4473
+ addLog('Simulation complete');
2727
4474
  renderSystemShift(r);
2728
4475
  renderRuleImpacts(r);
2729
4476
  renderEnforcementClassification(r.enforcementClassification || []);
4477
+
4478
+ // Observation Deck: post-run views
4479
+ var deckStatusEl2 = document.getElementById('deck-status');
4480
+ if (deckStatusEl2) { deckStatusEl2.textContent = 'Complete'; deckStatusEl2.className = 'deck-status complete'; }
4481
+ renderBehavioralDrift();
4482
+ renderWhatIf(r);
2730
4483
  lastSimResult = {
2731
4484
  stability: r.governed.metrics.stabilityScore,
2732
4485
  volatility: r.governed.metrics.maxVolatility,
@@ -2735,7 +4488,88 @@ function handleEvent(event) {
2735
4488
  };
2736
4489
  }
2737
4490
  runBtn.disabled = false;
2738
- runBtn.textContent = 'Run Simulation';
4491
+ runBtn.textContent = 'Run Governance';
4492
+
4493
+ // Generate post-run governance suggestions
4494
+ generateGovernanceSuggestions(r);
4495
+ }
4496
+ }
4497
+
4498
+ // ============================================
4499
+ // POST-RUN GOVERNANCE SUGGESTIONS
4500
+ // ============================================
4501
+ function generateGovernanceSuggestions(result) {
4502
+ var suggestionsSection = document.getElementById('governance-suggestions-section');
4503
+ var suggestionsEl = document.getElementById('governance-suggestions');
4504
+ if (!suggestionsSection || !suggestionsEl) return;
4505
+
4506
+ var suggestions = [];
4507
+ var thesis = currentThesis || '';
4508
+
4509
+ if (result && result.governed && result.baseline) {
4510
+ var gov = result.governed.metrics;
4511
+ var base = result.baseline.metrics;
4512
+ var effectiveness = result.comparison ? result.comparison.governanceEffectiveness : 0;
4513
+
4514
+ // Analyze what happened and suggest improvements
4515
+ if (gov.collapseProbability > 0.3) {
4516
+ suggestions.push({
4517
+ type: 'warning',
4518
+ text: 'Collapse probability is ' + (gov.collapseProbability * 100).toFixed(0) + '% even with governance. Consider adding stricter gates or lower thresholds.',
4519
+ });
4520
+ }
4521
+
4522
+ if (effectiveness < 0.3) {
4523
+ suggestions.push({
4524
+ type: 'warning',
4525
+ text: 'Governance effectiveness is low (' + (effectiveness * 100).toFixed(0) + '%). Your rules may be too loose or not matching agent behaviors. Consider tightening intent patterns.',
4526
+ });
4527
+ } else if (effectiveness > 0.8) {
4528
+ suggestions.push({
4529
+ type: 'success',
4530
+ text: 'Strong governance effectiveness (' + (effectiveness * 100).toFixed(0) + '%). Rules are actively shaping behavior.',
4531
+ });
4532
+ }
4533
+
4534
+ if (gov.stabilityScore > base.stabilityScore) {
4535
+ var delta = ((gov.stabilityScore - base.stabilityScore) * 100).toFixed(0);
4536
+ suggestions.push({
4537
+ type: 'success',
4538
+ text: 'Governance improved stability by ' + delta + ' points (from ' + (base.stabilityScore * 100).toFixed(0) + '% to ' + (gov.stabilityScore * 100).toFixed(0) + '%).',
4539
+ });
4540
+ }
4541
+
4542
+ // Check which rules never fired
4543
+ var unfiredRules = [];
4544
+ Object.keys(ruleImpactTracker).forEach(function(ruleId) {
4545
+ if (ruleImpactTracker[ruleId].blocks === 0) {
4546
+ unfiredRules.push(ruleImpactTracker[ruleId].label);
4547
+ }
4548
+ });
4549
+ if (unfiredRules.length > 0) {
4550
+ suggestions.push({
4551
+ type: 'info',
4552
+ text: unfiredRules.length + ' rule' + (unfiredRules.length > 1 ? 's' : '') + ' never triggered: ' + unfiredRules.slice(0, 3).join(', ') + (unfiredRules.length > 3 ? '...' : '') + '. Agents may already comply, or thresholds may be too high.',
4553
+ });
4554
+ }
4555
+
4556
+ // Thesis-aware suggestion
4557
+ if (thesis) {
4558
+ suggestions.push({
4559
+ type: 'thesis',
4560
+ text: 'Your goal: "' + thesis + '". ' + (effectiveness > 0.5 ? 'Current rules are aligned with this goal.' : 'Consider adjusting rules to better match this intent.'),
4561
+ });
4562
+ }
4563
+ }
4564
+
4565
+ if (suggestions.length > 0) {
4566
+ suggestionsSection.style.display = '';
4567
+ var colorMap = { warning: 'var(--yellow)', success: 'var(--green)', info: 'var(--accent)', thesis: '#818cf8' };
4568
+ suggestionsEl.innerHTML = suggestions.map(function(s) {
4569
+ return '<div class="suggestion-item" style="border-left:3px solid ' + (colorMap[s.type] || 'var(--text-muted)') + '">' +
4570
+ '<div class="suggestion-text">' + s.text + '</div>' +
4571
+ '</div>';
4572
+ }).join('');
2739
4573
  }
2740
4574
  }
2741
4575
 
@@ -2805,20 +4639,37 @@ const ssImpactsEl = document.getElementById('ss-impacts');
2805
4639
  const ssNarrativeEl = document.getElementById('ss-narrative');
2806
4640
 
2807
4641
  // Raw detail toggle
2808
- document.getElementById('ss-raw-toggle').addEventListener('click', function() {
2809
- this.classList.toggle('open');
2810
- document.getElementById('ss-raw-detail').classList.toggle('open');
2811
- });
4642
+ var ssRawToggle = document.getElementById('ss-raw-toggle');
4643
+ if (ssRawToggle) {
4644
+ ssRawToggle.addEventListener('click', function() {
4645
+ this.classList.toggle('open');
4646
+ document.getElementById('ss-raw-detail').classList.toggle('open');
4647
+ });
4648
+ }
2812
4649
 
2813
4650
  let shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
2814
4651
 
2815
4652
  function resetShiftTracker() {
2816
4653
  shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
2817
- ssCard.classList.remove('visible');
4654
+ if (ssCard) ssCard.classList.remove('visible');
2818
4655
  var rawToggle = document.getElementById('ss-raw-toggle');
2819
4656
  var rawDetail = document.getElementById('ss-raw-detail');
2820
4657
  if (rawToggle) rawToggle.classList.remove('open');
2821
4658
  if (rawDetail) rawDetail.classList.remove('open');
4659
+ // Reset behavior tracking
4660
+ behaviorLog = [];
4661
+ totalActions = 0;
4662
+ totalInterventions = 0;
4663
+ latestStability = 0;
4664
+ latestVolatility = 0;
4665
+ // Reset visible panels
4666
+ if (outcomeStatementEl) outcomeStatementEl.innerHTML = '<span class="outcome-empty">Run a simulation or connect an agent to see outcomes</span>';
4667
+ if (confidenceGridEl) confidenceGridEl.style.display = 'none';
4668
+ if (outcomeContextEl) outcomeContextEl.textContent = '';
4669
+ if (behaviorShiftsEl) behaviorShiftsEl.innerHTML = '<div class="behavior-empty">Waiting for agent actions</div>';
4670
+ if (activityToggleEl) activityToggleEl.style.display = 'none';
4671
+ if (activityTimelineEl) { activityTimelineEl.innerHTML = ''; activityTimelineEl.classList.remove('open'); }
4672
+ if (whyContentEl) whyContentEl.innerHTML = '<div class="why-empty">Causation analysis appears after agent actions are evaluated</div>';
2822
4673
  }
2823
4674
 
2824
4675
  function trackShift(event) {
@@ -2861,6 +4712,7 @@ function trackShift(event) {
2861
4712
 
2862
4713
  function renderSystemShift(result) {
2863
4714
  if (shiftTracker.blocks === 0) return;
4715
+ if (!ssCard) return; // System shift card removed from visible UI
2864
4716
 
2865
4717
  var adaptRate = shiftTracker.total > 0 ? Math.round((shiftTracker.blocks / shiftTracker.total) * 100) : 0;
2866
4718
 
@@ -2992,11 +4844,11 @@ cancelSaveBtn.addEventListener('click', () => {
2992
4844
 
2993
4845
  confirmSaveBtn.addEventListener('click', async () => {
2994
4846
  const name = variantNameInput.value.trim();
2995
- if (!name || !currentWorld) return;
4847
+ if (!name) return;
2996
4848
 
2997
4849
  // Gather current state overrides
2998
4850
  const stateOverrides = {};
2999
- if (currentWorld.stateVariables) {
4851
+ if (currentWorld && currentWorld.stateVariables) {
3000
4852
  currentWorld.stateVariables.forEach(sv => {
3001
4853
  const el = document.getElementById('sv-' + sv.id);
3002
4854
  if (!el) return;
@@ -3006,13 +4858,17 @@ confirmSaveBtn.addEventListener('click', async () => {
3006
4858
  });
3007
4859
  }
3008
4860
 
4861
+ // Save as SimulationRules (complete snapshot)
3009
4862
  const payload = {
3010
4863
  name,
3011
4864
  description: variantDescInput.value.trim(),
3012
- baseWorld: currentWorld.id,
4865
+ thesis: currentThesis,
4866
+ bridgeId: connectedBridge ? connectedBridge.id : undefined,
4867
+ rules: currentRules.map(function(r) {
4868
+ return { id: r.id, description: r.description, enforcement: r.enforcement, intent_patterns: r.intent_patterns || [] };
4869
+ }),
4870
+ worldDefinition: currentWorld || undefined,
3013
4871
  stateOverrides,
3014
- events: injectedEvents.slice(),
3015
- rounds: parseInt(roundsSlider.value),
3016
4872
  lastResult: lastSimResult,
3017
4873
  };
3018
4874
 
@@ -3045,18 +4901,21 @@ async function loadVariants() {
3045
4901
 
3046
4902
  function renderVariants(variants) {
3047
4903
  if (variants.length === 0) {
3048
- variantListEl.innerHTML = '<div style="font-size:11px;color:#333">No saved variants yet</div>';
4904
+ variantListEl.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No saved rules yet</div>';
3049
4905
  return;
3050
4906
  }
3051
4907
  variantListEl.innerHTML = variants.map(v => {
3052
4908
  const resultHtml = v.lastResult
3053
4909
  ? '<span class="vresult">Stability: ' + (v.lastResult.stability * 100).toFixed(0) + '% | Effectiveness: ' + (v.lastResult.governanceEffectiveness * 100).toFixed(0) + '%</span>'
3054
- : '<span style="color:#555">Not yet run</span>';
4910
+ : '<span style="color:var(--text-faint)">Not yet run</span>';
4911
+ const rulesCount = v.rules ? v.rules.length + ' rules' : (v.events ? v.events.length + ' events' : '');
4912
+ const bridgeLabel = v.bridgeId ? '<span class="vbase">' + v.bridgeId + '</span>' : (v.baseWorld ? '<span class="vbase">' + v.baseWorld + '</span>' : '');
3055
4913
  return '<div class="variant-card" data-vid="' + v.id + '">' +
3056
4914
  '<div class="vname">' + v.name + '</div>' +
4915
+ (v.thesis ? '<div class="vdesc" style="font-style:italic">' + v.thesis + '</div>' : '') +
3057
4916
  (v.description ? '<div class="vdesc">' + v.description + '</div>' : '') +
3058
- '<span class="vbase">' + v.baseWorld + '</span>' +
3059
- '<div class="vmeta">' + resultHtml + ' | ' + v.events.length + ' events | ' + v.rounds + ' rounds</div>' +
4917
+ bridgeLabel +
4918
+ '<div class="vmeta">' + resultHtml + (rulesCount ? ' | ' + rulesCount : '') + '</div>' +
3060
4919
  '<span class="vdelete" data-vid="' + v.id + '">delete</span>' +
3061
4920
  '</div>';
3062
4921
  }).join('');
@@ -3087,38 +4946,49 @@ function renderVariants(variants) {
3087
4946
  }
3088
4947
 
3089
4948
  function loadVariant(variant) {
3090
- // Set world
3091
- selectWorld(variant.baseWorld);
3092
-
3093
- // Apply state overrides
3094
- if (variant.stateOverrides && currentWorld && currentWorld.stateVariables) {
3095
- currentWorld.stateVariables.forEach(sv => {
3096
- if (sv.id in variant.stateOverrides) {
3097
- const el = document.getElementById('sv-' + sv.id);
3098
- if (!el) return;
3099
- if (sv.type === 'boolean') {
3100
- const val = variant.stateOverrides[sv.id];
3101
- if (val && !el.classList.contains('on')) el.classList.add('on');
3102
- if (!val && el.classList.contains('on')) el.classList.remove('on');
3103
- } else {
3104
- el.value = variant.stateOverrides[sv.id];
3105
- // Update value display for sliders
3106
- const valEl = document.getElementById('sv-val-' + sv.id);
3107
- if (valEl) valEl.textContent = variant.stateOverrides[sv.id];
3108
- }
3109
- }
3110
- });
3111
- }
4949
+ // Handle new SimulationRules format
4950
+ if (variant.thesis !== undefined) {
4951
+ // Restore thesis
4952
+ currentThesis = variant.thesis;
4953
+ if (thesisInput) thesisInput.value = variant.thesis;
4954
+
4955
+ // Restore rules
4956
+ if (variant.rules && variant.rules.length > 0) {
4957
+ currentRules = variant.rules;
4958
+ // Re-apply rules to server
4959
+ fetch('/api/generate-rules', {
4960
+ method: 'POST',
4961
+ headers: { 'Content-Type': 'application/json' },
4962
+ body: JSON.stringify({ thesis: variant.thesis }),
4963
+ }).catch(() => {});
4964
+
4965
+ // Show rules preview
4966
+ var iconMap = { block: '&#x1F534;', allow: '&#x1F7E2;', modify: '&#x1F535;', warn: '&#x1F7E1;' };
4967
+ generatedRulesPreviewEl.innerHTML = variant.rules.map(function(r) {
4968
+ var icon = iconMap[r.enforcement] || '&#x1F7E2;';
4969
+ return '<div class="parsed-rule enforcement-' + r.enforcement + '">' +
4970
+ '<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + (r.enforcement || 'rule') + '</span></div>' +
4971
+ '<div class="pr-desc">' + r.description + '</div>' +
4972
+ '</div>';
4973
+ }).join('');
3112
4974
 
3113
- // Set events
3114
- injectedEvents = variant.events.slice();
3115
- renderInjectedEvents();
4975
+ ruleGenStatusEl.textContent = variant.rules.length + ' rule(s) loaded from "' + variant.name + '".';
4976
+ ruleGenStatusEl.className = 'rule-status success';
4977
+ }
3116
4978
 
3117
- // Set rounds
3118
- roundsSlider.value = Math.min(variant.rounds, 12);
3119
- roundsVal.textContent = Math.min(variant.rounds, 12);
4979
+ // Restore world definition
4980
+ if (variant.worldDefinition) {
4981
+ currentWorld = variant.worldDefinition;
4982
+ renderStateVars();
4983
+ }
4984
+ } else {
4985
+ // Legacy WorldVariant format — try to load base world
4986
+ if (variant.baseWorld) {
4987
+ selectWorld(variant.baseWorld);
4988
+ }
4989
+ }
3120
4990
 
3121
- addLog('Loaded variant: ' + variant.name, '');
4991
+ addLog('Loaded rules: ' + variant.name, '');
3122
4992
  }
3123
4993
 
3124
4994
  // ============================================
@@ -3151,18 +5021,6 @@ function applyTheme(theme) {
3151
5021
  themeToggleBtn.textContent = 'Light Mode';
3152
5022
  }
3153
5023
  localStorage.setItem('nv-theme', theme);
3154
- // Update chart colors if chart exists
3155
- if (chart && chart.options) {
3156
- const gridColor = theme === 'light' ? '#d4d4d4' : '#2a2a2a';
3157
- const tickColor = theme === 'light' ? '#666' : '#888';
3158
- const legendColor = theme === 'light' ? '#444' : '#888';
3159
- chart.options.scales.x.ticks.color = tickColor;
3160
- chart.options.scales.x.grid.color = gridColor;
3161
- chart.options.scales.y.ticks.color = tickColor;
3162
- chart.options.scales.y.grid.color = gridColor;
3163
- chart.options.plugins.legend.labels.color = legendColor;
3164
- chart.update();
3165
- }
3166
5024
  }
3167
5025
  themeToggleBtn.addEventListener('click', () => {
3168
5026
  const current = document.body.classList.contains('light') ? 'light' : 'dark';
@@ -3173,129 +5031,15 @@ const savedTheme = localStorage.getItem('nv-theme');
3173
5031
  if (savedTheme) applyTheme(savedTheme);
3174
5032
 
3175
5033
  // ============================================
3176
- // WORLD SOURCE SWITCHING
5034
+ // (Old world source switching removed — replaced by thesis + bridge flow)
3177
5035
  // ============================================
3178
- let currentWorldSource = 'preset';
3179
- const worldSourceTabs = document.querySelectorAll('.ws-tab');
3180
- const sourcePresetPanel = document.getElementById('source-preset');
3181
- const sourceCustomPanel = document.getElementById('source-custom');
3182
- const sourceUploadPanel = document.getElementById('source-upload');
3183
-
3184
- worldSourceTabs.forEach(tab => {
3185
- tab.addEventListener('click', () => {
3186
- const source = tab.dataset.source;
3187
- if (source === currentWorldSource) return;
3188
-
3189
- currentWorldSource = source;
3190
-
3191
- // Update tab visuals
3192
- worldSourceTabs.forEach(t => t.classList.remove('active'));
3193
- tab.classList.add('active');
3194
- tab.querySelector('input').checked = true;
3195
-
3196
- // Show/hide panels
3197
- sourcePresetPanel.style.display = source === 'preset' ? '' : 'none';
3198
- sourceCustomPanel.style.display = source === 'custom' ? '' : 'none';
3199
- sourceUploadPanel.style.display = source === 'upload' ? '' : 'none';
3200
- });
3201
- });
3202
-
3203
- // Populate base world selector in custom rules panel
3204
- function populateBaseWorldSelect() {
3205
- const select = document.getElementById('custom-base-world');
3206
- if (!select) return;
3207
- worlds.forEach(w => {
3208
- const opt = document.createElement('option');
3209
- opt.value = w.id;
3210
- opt.textContent = w.title;
3211
- select.appendChild(opt);
3212
- });
3213
- }
3214
5036
 
3215
5037
  // ============================================
3216
- // WORLD ACTION BAR
5038
+ // (Old world action bar removed — replaced by thesis + bridge flow)
3217
5039
  // ============================================
3218
5040
 
3219
- // + New World
3220
- document.getElementById('new-world-btn').addEventListener('click', () => {
3221
- // Switch to custom rules mode
3222
- currentWorldSource = 'custom';
3223
- worldSourceTabs.forEach(t => {
3224
- t.classList.toggle('active', t.dataset.source === 'custom');
3225
- t.querySelector('input').checked = t.dataset.source === 'custom';
3226
- });
3227
- sourcePresetPanel.style.display = 'none';
3228
- sourceCustomPanel.style.display = '';
3229
- sourceUploadPanel.style.display = 'none';
3230
-
3231
- // Clear everything
3232
- document.getElementById('custom-world-name').value = '';
3233
- document.getElementById('custom-world-thesis').value = '';
3234
- document.getElementById('rule-input').value = '';
3235
- document.getElementById('parsed-rules').innerHTML = '';
3236
- document.getElementById('rule-status').textContent = '';
3237
- document.getElementById('rule-status').className = 'rule-status';
3238
- document.getElementById('custom-base-world').value = '';
3239
-
3240
- // Clear active rules server-side
3241
- fetch('/api/clear-rules', { method: 'POST' });
3242
-
3243
- // Reset right panel
3244
- document.getElementById('active-invariants').innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No rules loaded. Define your world.</div>';
3245
- });
3246
-
3247
- // Clear Rules
3248
- document.getElementById('clear-rules-btn').addEventListener('click', async () => {
3249
- await fetch('/api/clear-rules', { method: 'POST' });
3250
-
3251
- // Clear rule editor UI
3252
- const ruleInput = document.getElementById('rule-input');
3253
- if (ruleInput) ruleInput.value = '';
3254
- const parsed = document.getElementById('parsed-rules');
3255
- if (parsed) parsed.innerHTML = '';
3256
- const status = document.getElementById('rule-status');
3257
- if (status) { status.textContent = 'Rules cleared.'; status.className = 'rule-status success'; }
3258
-
3259
- // Clear upload state
3260
- const uploadStatus = document.getElementById('upload-status');
3261
- if (uploadStatus) { uploadStatus.textContent = 'Rules cleared.'; uploadStatus.className = 'rule-status success'; }
3262
- const loadedInfo = document.getElementById('loaded-world-info');
3263
- if (loadedInfo) loadedInfo.style.display = 'none';
3264
- });
3265
-
3266
- // Load World File (switch to upload tab)
3267
- document.getElementById('load-file-btn').addEventListener('click', () => {
3268
- currentWorldSource = 'upload';
3269
- worldSourceTabs.forEach(t => {
3270
- t.classList.toggle('active', t.dataset.source === 'upload');
3271
- t.querySelector('input').checked = t.dataset.source === 'upload';
3272
- });
3273
- sourcePresetPanel.style.display = 'none';
3274
- sourceCustomPanel.style.display = 'none';
3275
- sourceUploadPanel.style.display = '';
3276
- });
3277
-
3278
- // Save as World File (export)
3279
- document.getElementById('export-world-btn').addEventListener('click', async () => {
3280
- try {
3281
- const resp = await fetch('/api/export-world');
3282
- const data = await resp.json();
3283
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
3284
- const url = URL.createObjectURL(blob);
3285
- const a = document.createElement('a');
3286
- a.href = url;
3287
- a.download = (currentWorld ? currentWorld.id : 'custom') + '-world.json';
3288
- document.body.appendChild(a);
3289
- a.click();
3290
- document.body.removeChild(a);
3291
- URL.revokeObjectURL(url);
3292
- } catch (err) {
3293
- alert('Export failed: ' + err.message);
3294
- }
3295
- });
3296
-
3297
5041
  // ============================================
3298
- // WORLD FILE UPLOAD / PASTE
5042
+ // WORLD FILE UPLOAD / PASTE (simplified)
3299
5043
  // ============================================
3300
5044
  const uploadZone = document.getElementById('upload-zone');
3301
5045
  const uploadFileInput = document.getElementById('upload-file-input');
@@ -3304,56 +5048,43 @@ const worldJsonInput = document.getElementById('world-json-input');
3304
5048
  const loadWorldBtn = document.getElementById('load-world-btn');
3305
5049
  const uploadStatusEl = document.getElementById('upload-status');
3306
5050
 
3307
- // Drag and drop
3308
- uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
3309
- uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
3310
- uploadZone.addEventListener('drop', (e) => {
3311
- e.preventDefault();
3312
- uploadZone.classList.remove('dragover');
3313
- const file = e.dataTransfer.files[0];
3314
- if (file) readWorldFile(file);
3315
- });
3316
-
3317
- // Browse button
3318
- uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadFileInput.click(); });
3319
- uploadFileInput.addEventListener('change', () => {
3320
- if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]);
3321
- });
3322
-
3323
- // Click zone to browse
3324
- uploadZone.addEventListener('click', () => { uploadFileInput.click(); });
5051
+ if (uploadZone) {
5052
+ uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
5053
+ uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
5054
+ uploadZone.addEventListener('drop', (e) => {
5055
+ e.preventDefault();
5056
+ uploadZone.classList.remove('dragover');
5057
+ const file = e.dataTransfer.files[0];
5058
+ if (file) readWorldFile(file);
5059
+ });
5060
+ uploadZone.addEventListener('click', () => { if (uploadFileInput) uploadFileInput.click(); });
5061
+ }
5062
+ if (uploadBrowseBtn) uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); if (uploadFileInput) uploadFileInput.click(); });
5063
+ if (uploadFileInput) uploadFileInput.addEventListener('change', () => { if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]); });
3325
5064
 
3326
5065
  function readWorldFile(file) {
3327
5066
  const reader = new FileReader();
3328
5067
  reader.onload = (e) => {
3329
- worldJsonInput.value = e.target.result;
3330
- uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".';
3331
- uploadStatusEl.className = 'rule-status success';
5068
+ if (worldJsonInput) worldJsonInput.value = e.target.result;
5069
+ if (uploadStatusEl) { uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".'; uploadStatusEl.className = 'rule-status success'; }
3332
5070
  };
3333
5071
  reader.readAsText(file);
3334
5072
  }
3335
5073
 
3336
- // Load into Runtime
3337
- loadWorldBtn.addEventListener('click', async () => {
3338
- const jsonText = worldJsonInput.value.trim();
5074
+ if (loadWorldBtn) loadWorldBtn.addEventListener('click', async () => {
5075
+ const jsonText = worldJsonInput ? worldJsonInput.value.trim() : '';
3339
5076
  if (!jsonText) {
3340
- uploadStatusEl.textContent = 'Paste or upload a world file first.';
3341
- uploadStatusEl.className = 'rule-status error';
5077
+ if (uploadStatusEl) { uploadStatusEl.textContent = 'Paste or upload a world file first.'; uploadStatusEl.className = 'rule-status error'; }
3342
5078
  return;
3343
5079
  }
3344
5080
 
3345
5081
  let worldData;
3346
- try {
3347
- worldData = JSON.parse(jsonText);
3348
- } catch (err) {
3349
- uploadStatusEl.textContent = 'Invalid JSON: ' + err.message;
3350
- uploadStatusEl.className = 'rule-status error';
5082
+ try { worldData = JSON.parse(jsonText); } catch (err) {
5083
+ if (uploadStatusEl) { uploadStatusEl.textContent = 'Invalid JSON: ' + err.message; uploadStatusEl.className = 'rule-status error'; }
3351
5084
  return;
3352
5085
  }
3353
5086
 
3354
- // Normalize: if the JSON is { world: {...} } or just {...}
3355
5087
  const worldPayload = worldData.world || worldData;
3356
-
3357
5088
  loadWorldBtn.textContent = 'Loading...';
3358
5089
  loadWorldBtn.disabled = true;
3359
5090
 
@@ -3366,176 +5097,50 @@ loadWorldBtn.addEventListener('click', async () => {
3366
5097
  const result = await resp.json();
3367
5098
 
3368
5099
  if (result.error) {
3369
- uploadStatusEl.textContent = result.error;
3370
- uploadStatusEl.className = 'rule-status error';
5100
+ if (uploadStatusEl) { uploadStatusEl.textContent = result.error; uploadStatusEl.className = 'rule-status error'; }
3371
5101
  } else {
3372
- uploadStatusEl.textContent = result.message;
3373
- uploadStatusEl.className = 'rule-status success';
5102
+ if (uploadStatusEl) { uploadStatusEl.textContent = result.message; uploadStatusEl.className = 'rule-status success'; }
3374
5103
 
3375
5104
  // Show loaded world info
3376
- const infoEl = document.getElementById('loaded-world-info');
3377
- infoEl.style.display = '';
3378
- document.getElementById('lw-name').textContent = result.world.title;
3379
- document.getElementById('lw-thesis').textContent = '"' + result.world.thesis + '"';
3380
- document.getElementById('lw-stats').textContent =
3381
- result.world.invariants.length + ' invariants, ' +
3382
- result.world.gates.length + ' gates, ' +
3383
- result.rulesApplied + ' rules';
3384
-
3385
- // Update active invariants in right panel
3386
- const invHtml = result.world.invariants.map(inv =>
5105
+ var infoEl = document.getElementById('loaded-world-info');
5106
+ if (infoEl) infoEl.style.display = '';
5107
+ var lwName = document.getElementById('lw-name');
5108
+ if (lwName) lwName.textContent = result.world.title;
5109
+ var lwThesis = document.getElementById('lw-thesis');
5110
+ if (lwThesis) lwThesis.textContent = '"' + result.world.thesis + '"';
5111
+ var lwStats = document.getElementById('lw-stats');
5112
+ if (lwStats) lwStats.textContent = result.world.invariants.length + ' invariants, ' + result.world.gates.length + ' gates, ' + result.rulesApplied + ' rules';
5113
+
5114
+ // Set thesis from loaded world
5115
+ if (result.world.thesis && thesisInput) {
5116
+ thesisInput.value = result.world.thesis;
5117
+ currentThesis = result.world.thesis;
5118
+ }
5119
+
5120
+ // Set current world
5121
+ currentWorld = {
5122
+ id: 'loaded-world',
5123
+ title: result.world.title,
5124
+ thesis: result.world.thesis,
5125
+ stateVariables: result.world.stateVariables || [],
5126
+ invariants: result.world.invariants || [],
5127
+ gates: result.world.gates || [],
5128
+ };
5129
+ renderStateVars();
5130
+
5131
+ // Update active invariants (hidden, for audit)
5132
+ activeInvEl.innerHTML = (result.world.invariants || []).map(inv =>
3387
5133
  '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
3388
- ).join('') + result.world.gates.map(g =>
3389
- '<div class="inv-item" style="color:' + (g.severity === 'critical' ? 'var(--red)' : 'var(--yellow)') + '">[' + g.id + '] ' + g.label + '</div>'
3390
5134
  ).join('');
3391
- activeInvEl.innerHTML = invHtml || '<div style="font-size:11px;color:var(--text-muted)">No invariants defined</div>';
3392
-
3393
- // Update state variables if present
3394
- if (result.world.stateVariables && result.world.stateVariables.length > 0) {
3395
- // Store as a pseudo-world so sliders render
3396
- currentWorld = {
3397
- id: 'custom-world',
3398
- title: result.world.title,
3399
- thesis: result.world.thesis,
3400
- stateVariables: result.world.stateVariables,
3401
- invariants: result.world.invariants,
3402
- gates: result.world.gates,
3403
- };
3404
- selectWorld('custom-world');
3405
- } else {
3406
- // Just set current world reference
3407
- currentWorld = {
3408
- id: 'custom-world',
3409
- title: result.world.title,
3410
- thesis: result.world.thesis,
3411
- stateVariables: [],
3412
- invariants: result.world.invariants,
3413
- gates: result.world.gates,
3414
- };
3415
- }
3416
5135
  }
3417
5136
  } catch (err) {
3418
- uploadStatusEl.textContent = 'Error: ' + err.message;
3419
- uploadStatusEl.className = 'rule-status error';
5137
+ if (uploadStatusEl) { uploadStatusEl.textContent = 'Error: ' + err.message; uploadStatusEl.className = 'rule-status error'; }
3420
5138
  }
3421
5139
 
3422
5140
  loadWorldBtn.textContent = 'Load into Runtime';
3423
5141
  loadWorldBtn.disabled = false;
3424
5142
  });
3425
5143
 
3426
- // ============================================
3427
- // PLAIN-ENGLISH RULE EDITOR
3428
- // ============================================
3429
- const ruleInput = document.getElementById('rule-input');
3430
- const parseRulesBtn = document.getElementById('parse-rules-btn');
3431
- const parsedRulesEl = document.getElementById('parsed-rules');
3432
- const ruleStatusEl = document.getElementById('rule-status');
3433
- let parsedRuleData = [];
3434
-
3435
- parseRulesBtn.addEventListener('click', async () => {
3436
- const text = ruleInput.value.trim();
3437
- if (!text) return;
3438
-
3439
- parseRulesBtn.disabled = true;
3440
- parseRulesBtn.textContent = 'Parsing...';
3441
- ruleStatusEl.textContent = '';
3442
- ruleStatusEl.className = 'rule-status';
3443
-
3444
- try {
3445
- const resp = await fetch('/api/parse-rules', {
3446
- method: 'POST',
3447
- headers: { 'Content-Type': 'application/json' },
3448
- body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
3449
- });
3450
- const data = await resp.json();
3451
-
3452
- if (data.error) {
3453
- ruleStatusEl.textContent = data.error;
3454
- ruleStatusEl.className = 'rule-status error';
3455
- parsedRulesEl.innerHTML = '';
3456
- parsedRuleData = [];
3457
- } else {
3458
- parsedRuleData = data.rules || [];
3459
- parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
3460
- const enfType = r.enforcement || 'block';
3461
- const iconMap = { block: '&#x1F534;', allow: '&#x1F7E2;', modify: '&#x1F535;', warn: '&#x1F7E1;', pause: '&#x1F7E1;' };
3462
- const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
3463
- const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
3464
- const icon = iconMap[enfType] || '&#x1F7E2;';
3465
- const label = labelMap[enfType] || 'Rule';
3466
- const effect = effectMap[enfType] || 'Active';
3467
- return '<div class="parsed-rule enforcement-' + enfType + '">' +
3468
- '<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
3469
- '<div class="pr-desc">' + r.description + '</div>' +
3470
- '<div class="pr-patterns">' + effect + ' &bull; Matches: ' + r.intent_patterns.join(', ') + '</div>' +
3471
- '</div>';
3472
- }).join('');
3473
-
3474
- if (parsedRuleData.length > 0) {
3475
- const btnLabel = currentWorldSource === 'custom' ? 'Generate World with ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') : 'Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation';
3476
- parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">' + btnLabel + '</button>';
3477
- document.getElementById('apply-rules-btn').addEventListener('click', async () => {
3478
- try {
3479
- // If in custom rules mode, use base world if selected
3480
- let worldId = currentWorld ? currentWorld.id : 'trading';
3481
- if (currentWorldSource === 'custom') {
3482
- const baseWorld = document.getElementById('custom-base-world').value;
3483
- if (baseWorld) worldId = baseWorld;
3484
- }
3485
-
3486
- const applyResp = await fetch('/api/apply-rules', {
3487
- method: 'POST',
3488
- headers: { 'Content-Type': 'application/json' },
3489
- body: JSON.stringify({ rules: parsedRuleData, worldId }),
3490
- });
3491
- const applyData = await applyResp.json();
3492
- if (applyData.status === 'applied') {
3493
- ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
3494
- ruleStatusEl.className = 'rule-status success';
3495
-
3496
- // Update right panel invariants with custom rules
3497
- const customName = document.getElementById('custom-world-name');
3498
- const worldName = (customName && customName.value) ? customName.value : 'Custom World';
3499
- const customThesis = document.getElementById('custom-world-thesis');
3500
- const thesis = (customThesis && customThesis.value) ? customThesis.value : 'User-defined governance rules';
3501
-
3502
- // Show rules in active invariants panel
3503
- activeInvEl.innerHTML = parsedRuleData.map(r => {
3504
- const enfType = r.enforcement || 'block';
3505
- const colorMap = { block: 'var(--red)', allow: 'var(--green)', modify: 'var(--blue)', warn: 'var(--yellow)', pause: 'var(--yellow)' };
3506
- const color = colorMap[enfType] || 'var(--text-secondary)';
3507
- return '<div class="inv-item" style="color:' + color + '">[' + r.id + '] ' + r.description + '</div>';
3508
- }).join('');
3509
-
3510
- // In custom mode, set a custom world reference
3511
- if (currentWorldSource === 'custom') {
3512
- const baseWorld = document.getElementById('custom-base-world').value;
3513
- if (baseWorld) {
3514
- selectWorld(baseWorld);
3515
- } else {
3516
- currentWorld = { id: 'custom-world', title: worldName, thesis, stateVariables: [], invariants: [], gates: [] };
3517
- }
3518
- document.getElementById('world-thesis').textContent = '"' + thesis + '"';
3519
- }
3520
- }
3521
- } catch (err) {
3522
- ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
3523
- ruleStatusEl.className = 'rule-status error';
3524
- }
3525
- });
3526
- ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
3527
- ruleStatusEl.className = 'rule-status success';
3528
- }
3529
- }
3530
- } catch (err) {
3531
- ruleStatusEl.textContent = 'Error: ' + err.message;
3532
- ruleStatusEl.className = 'rule-status error';
3533
- }
3534
-
3535
- parseRulesBtn.disabled = false;
3536
- parseRulesBtn.textContent = 'Parse Rules';
3537
- });
3538
-
3539
5144
  // ============================================
3540
5145
  // SESSION TRACKING
3541
5146
  // ============================================
@@ -3551,6 +5156,8 @@ async function pollSessionStats() {
3551
5156
  if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
3552
5157
  if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
3553
5158
  if (el('s-allowed')) el('s-allowed').textContent = data.evaluations.allowed;
5159
+ if (el('s-rewarded')) el('s-rewarded').textContent = data.evaluations.rewarded || 0;
5160
+ if (el('s-penalized')) el('s-penalized').textContent = data.evaluations.penalized || 0;
3554
5161
  if (el('s-agents')) {
3555
5162
  el('s-agents').textContent = data.agents.length > 0
3556
5163
  ? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
@@ -3612,6 +5219,189 @@ async function saveExperiment() {
3612
5219
  sessionPollInterval = setInterval(pollSessionStats, 2000);
3613
5220
  pollSessionStats();
3614
5221
 
5222
+ // ============================================
5223
+ // AI API KEY MANAGEMENT
5224
+ // ============================================
5225
+ var aiKeyInput = document.getElementById('ai-key-input');
5226
+ var aiKeyStatus = document.getElementById('ai-key-status');
5227
+ var aiKeySaveBtn = document.getElementById('ai-key-save-btn');
5228
+ var aiKeyExtra = document.getElementById('ai-key-extra');
5229
+ var aiBaseUrl = document.getElementById('ai-base-url');
5230
+ var aiModel = document.getElementById('ai-model');
5231
+ var selectedProvider = 'anthropic';
5232
+
5233
+ // Provider buttons
5234
+ document.querySelectorAll('.ai-key-provider-btn').forEach(function(btn) {
5235
+ btn.addEventListener('click', function() {
5236
+ document.querySelectorAll('.ai-key-provider-btn').forEach(function(b) { b.classList.remove('active'); });
5237
+ btn.classList.add('active');
5238
+ selectedProvider = btn.getAttribute('data-provider');
5239
+ // Show extra fields for custom/openai providers
5240
+ if (aiKeyExtra) {
5241
+ aiKeyExtra.style.display = (selectedProvider === 'custom' || selectedProvider === 'openai') ? '' : 'none';
5242
+ }
5243
+ // Update placeholder
5244
+ if (aiKeyInput) {
5245
+ if (selectedProvider === 'anthropic') aiKeyInput.placeholder = 'sk-ant-... (Anthropic API key)';
5246
+ else if (selectedProvider === 'openai') aiKeyInput.placeholder = 'sk-... (OpenAI API key)';
5247
+ else aiKeyInput.placeholder = 'API key (or "none" for local models)';
5248
+ }
5249
+ });
5250
+ });
5251
+
5252
+ // Load current config on boot
5253
+ fetch('/api/ai-config').then(function(r) { return r.json(); }).then(function(data) {
5254
+ if (data.hasKey && aiKeyStatus) {
5255
+ aiKeyStatus.textContent = data.keyPreview;
5256
+ aiKeyStatus.className = 'ai-key-status connected';
5257
+ }
5258
+ if (data.provider) {
5259
+ selectedProvider = data.provider;
5260
+ document.querySelectorAll('.ai-key-provider-btn').forEach(function(b) {
5261
+ b.classList.toggle('active', b.getAttribute('data-provider') === data.provider);
5262
+ });
5263
+ if (aiKeyExtra) {
5264
+ aiKeyExtra.style.display = (data.provider === 'custom' || data.provider === 'openai') ? '' : 'none';
5265
+ }
5266
+ }
5267
+ if (data.baseUrl && aiBaseUrl) aiBaseUrl.value = data.baseUrl;
5268
+ if (data.model && aiModel) aiModel.value = data.model;
5269
+ }).catch(function() {});
5270
+
5271
+ // Save key
5272
+ if (aiKeySaveBtn) aiKeySaveBtn.addEventListener('click', function() {
5273
+ var key = aiKeyInput ? aiKeyInput.value.trim() : '';
5274
+ if (!key) return;
5275
+ var payload = { provider: selectedProvider, apiKey: key };
5276
+ if (aiBaseUrl && aiBaseUrl.value.trim()) payload.baseUrl = aiBaseUrl.value.trim();
5277
+ if (aiModel && aiModel.value.trim()) payload.model = aiModel.value.trim();
5278
+
5279
+ fetch('/api/ai-config', {
5280
+ method: 'POST',
5281
+ headers: { 'Content-Type': 'application/json' },
5282
+ body: JSON.stringify(payload),
5283
+ }).then(function(r) { return r.json(); }).then(function(data) {
5284
+ if (data.status === 'updated' && aiKeyStatus) {
5285
+ aiKeyStatus.textContent = 'Connected';
5286
+ aiKeyStatus.className = 'ai-key-status connected';
5287
+ if (aiKeyInput) aiKeyInput.value = '';
5288
+ aiKeyInput.placeholder = 'Key saved — enter new key to change';
5289
+ }
5290
+ }).catch(function(err) {
5291
+ if (aiKeyStatus) { aiKeyStatus.textContent = 'Error'; aiKeyStatus.className = 'ai-key-status missing'; }
5292
+ });
5293
+ });
5294
+
5295
+ // ============================================
5296
+ // INCENTIVE TRACKER (Reward/Penalize)
5297
+ // ============================================
5298
+ var incentiveLog = []; // { agent, type, magnitude, cooldown, round, action }
5299
+
5300
+ function trackIncentive(reaction, round) {
5301
+ if (!reaction || !reaction.verdict) return;
5302
+ var v = reaction.verdict;
5303
+ // Check for incentive data (from governance engine consequence/reward)
5304
+ if (!v.incentive && v.status !== 'REWARD' && v.status !== 'PENALIZE') return;
5305
+
5306
+ var inc = v.incentive || {};
5307
+ var agentState = v.agentState || {};
5308
+
5309
+ incentiveLog.push({
5310
+ agent: reaction.stakeholder_id,
5311
+ type: inc.type || (v.status === 'REWARD' ? 'reward' : 'penalize'),
5312
+ magnitude: inc.magnitude || 1,
5313
+ cooldown: inc.cooldownRounds || agentState.cooldown || 0,
5314
+ round: round,
5315
+ action: reaction.reaction,
5316
+ description: inc.description || v.reason || '',
5317
+ influence: agentState.influence || 1.0,
5318
+ totalPenalties: agentState.penalties || 0,
5319
+ totalRewards: agentState.rewards || 0,
5320
+ });
5321
+ renderIncentiveTracker();
5322
+ }
5323
+
5324
+ function renderIncentiveTracker() {
5325
+ var section = document.getElementById('deck-incentive-section');
5326
+ var summaryEl = document.getElementById('incentive-summary');
5327
+ var agentsEl = document.getElementById('incentive-agents');
5328
+ if (!section || !summaryEl || !agentsEl) return;
5329
+ if (incentiveLog.length === 0) { section.style.display = 'none'; return; }
5330
+
5331
+ section.style.display = '';
5332
+
5333
+ var rewards = incentiveLog.filter(function(i) { return i.type === 'reward'; });
5334
+ var penalties = incentiveLog.filter(function(i) { return i.type === 'penalize'; });
5335
+
5336
+ // Count unique frozen agents (latest state)
5337
+ var latestStates = {};
5338
+ incentiveLog.forEach(function(i) { latestStates[i.agent] = i; });
5339
+ var frozenCount = Object.values(latestStates).filter(function(s) { return s.cooldown > 0; }).length;
5340
+
5341
+ summaryEl.innerHTML =
5342
+ '<div style="display:flex;gap:8px;margin-bottom:12px">' +
5343
+ '<div style="flex:1;text-align:center;padding:8px;background:rgba(74,222,128,0.1);border-radius:6px">' +
5344
+ '<div style="font-size:24px;font-weight:800;color:#4ade80">' + rewards.length + '</div>' +
5345
+ '<div style="font-size:10px;color:#86efac">Rewards</div></div>' +
5346
+ '<div style="flex:1;text-align:center;padding:8px;background:rgba(251,146,60,0.1);border-radius:6px">' +
5347
+ '<div style="font-size:24px;font-weight:800;color:#fb923c">' + penalties.length + '</div>' +
5348
+ '<div style="font-size:10px;color:#fdba74">Penalties</div></div>' +
5349
+ '<div style="flex:1;text-align:center;padding:8px;background:rgba(99,102,241,0.1);border-radius:6px">' +
5350
+ '<div style="font-size:24px;font-weight:800;color:#818cf8">' + frozenCount + '</div>' +
5351
+ '<div style="font-size:10px;color:#a5b4fc">Frozen</div></div>' +
5352
+ '</div>';
5353
+
5354
+ // Show per-agent incentive history (latest 10)
5355
+ var recent = incentiveLog.slice(-10).reverse();
5356
+ var agentHtml = recent.map(function(inc) {
5357
+ var icon = inc.type === 'reward' ? '&#x2605;' : '&#x26A0;';
5358
+ var cls = inc.type === 'reward' ? 'reward' : 'penalize';
5359
+ var cooldownStr = inc.cooldown > 0 ? ' <span style="color:#818cf8;font-size:9px">(frozen ' + inc.cooldown + 'r)</span>' : '';
5360
+ var influenceStr = inc.influence !== undefined && inc.influence !== 1.0 ? ' <span style="color:#a78bfa;font-size:9px">inf:' + inc.influence.toFixed(1) + '</span>' : '';
5361
+ var descStr = inc.description ? '<div style="font-size:9px;color:var(--text-faint);margin-top:2px;padding-left:4px">' + inc.description + '</div>' : '';
5362
+ return '<div class="stream-item" style="flex-wrap:wrap">' +
5363
+ '<span class="incentive-badge ' + cls + '">' + icon + ' ' + inc.type + '</span>' +
5364
+ '<span class="stream-agent">' + inc.agent + '</span>' +
5365
+ '<span class="stream-action">' + normalizeAction(inc.action) + cooldownStr + influenceStr + '</span>' +
5366
+ '<span class="stream-round">R' + inc.round + '</span>' +
5367
+ descStr +
5368
+ '</div>';
5369
+ }).join('');
5370
+
5371
+ agentsEl.innerHTML = agentHtml;
5372
+ }
5373
+
5374
+ // ============================================
5375
+ // ONBOARDING
5376
+ // ============================================
5377
+ var onboardOverlay = document.getElementById('onboard-overlay');
5378
+ var onboardStartBtn = document.getElementById('onboard-start-btn');
5379
+ var onboardDismiss = document.getElementById('onboard-dismiss');
5380
+ var startHereBtn = document.getElementById('start-here-btn');
5381
+
5382
+ function showOnboarding() {
5383
+ if (onboardOverlay) onboardOverlay.style.display = '';
5384
+ }
5385
+
5386
+ function hideOnboarding(permanent) {
5387
+ if (onboardOverlay) onboardOverlay.style.display = 'none';
5388
+ if (permanent) localStorage.setItem('nv-onboarded', '1');
5389
+ }
5390
+
5391
+ // "Start" = close and mark as seen
5392
+ if (onboardStartBtn) onboardStartBtn.addEventListener('click', function() { hideOnboarding(true); });
5393
+ // "Don't show again" = same
5394
+ if (onboardDismiss) onboardDismiss.addEventListener('click', function() { hideOnboarding(true); });
5395
+ // Close on backdrop click
5396
+ if (onboardOverlay) onboardOverlay.addEventListener('click', function(e) { if (e.target === onboardOverlay) hideOnboarding(false); });
5397
+ // "Start Here" button in left panel = always show
5398
+ if (startHereBtn) startHereBtn.addEventListener('click', showOnboarding);
5399
+
5400
+ // Show on first visit
5401
+ if (!localStorage.getItem('nv-onboarded')) {
5402
+ showOnboarding();
5403
+ }
5404
+
3615
5405
  // ============================================
3616
5406
  // BOOT
3617
5407
  // ============================================