@neuroverseos/nv-sim 0.1.9 → 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 (37) hide show
  1. package/README.md +90 -3
  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-D2ZrMny8.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/cli.js +89 -114
  12. package/dist/engine/dynamicsGovernance.js +4 -0
  13. package/dist/engine/fullGovernedLoop.js +16 -1
  14. package/dist/engine/goalEngine.js +3 -4
  15. package/dist/engine/governance.js +18 -0
  16. package/dist/engine/index.js +19 -28
  17. package/dist/engine/intentTranslator.js +281 -0
  18. package/dist/engine/liveAdapter.js +100 -18
  19. package/dist/engine/liveVisualizer.js +2071 -1023
  20. package/dist/engine/primeRadiant.js +2 -8
  21. package/dist/engine/reasoningEngine.js +2 -7
  22. package/dist/engine/scenarioCapsule.js +5 -5
  23. package/dist/engine/swarmSimulation.js +1 -9
  24. package/dist/engine/worldBridge.js +22 -8
  25. package/dist/index.html +2 -2
  26. package/dist/lib/reasoningEngine.js +17 -1
  27. package/dist/lib/simulationAdapter.js +11 -11
  28. package/dist/lib/swarmParser.js +1 -1
  29. package/dist/runtime/govern.js +160 -7
  30. package/dist/runtime/index.js +1 -4
  31. package/dist/runtime/types.js +91 -0
  32. package/package.json +23 -6
  33. package/dist/adapters/mirofish.js +0 -461
  34. package/dist/assets/index-B64NuIXu.css +0 -1
  35. package/dist/assets/index-BMkPevVr.js +0 -532
  36. package/dist/assets/mirotir-logo-DUexumBH.svg +0 -185
  37. 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");
@@ -186,24 +187,90 @@ function startInteractiveServer(port, onReady) {
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;
189
202
  // ── Bridge Metrics Tracker ──
190
203
  // Computes meaningful live metrics from external /api/evaluate calls
191
204
  let bridgeEvalCount = 0;
192
205
  let bridgeBlockCount = 0;
193
206
  let bridgeModifyCount = 0;
207
+ let bridgeRewardCount = 0;
208
+ let bridgePenalizeCount = 0;
194
209
  function computeBridgeMetrics(decision) {
195
210
  bridgeEvalCount++;
196
211
  if (decision === "BLOCK")
197
212
  bridgeBlockCount++;
198
213
  if (decision === "MODIFY")
199
214
  bridgeModifyCount++;
200
- const totalInterventions = bridgeBlockCount + bridgeModifyCount;
215
+ if (decision === "REWARD")
216
+ bridgeRewardCount++;
217
+ if (decision === "PENALIZE")
218
+ bridgePenalizeCount++;
219
+ const totalInterventions = bridgeBlockCount + bridgeModifyCount + bridgePenalizeCount;
201
220
  // Stability = ratio of non-blocked actions (higher = more stable)
202
- const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount) / bridgeEvalCount : 1;
203
- // Volatility = ratio of interventions (blocks + modifies) — more interventions = more volatile
221
+ const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount - bridgePenalizeCount) / bridgeEvalCount : 1;
222
+ // Volatility = ratio of interventions (blocks + modifies + penalizes) — more interventions = more volatile
204
223
  const volatility = bridgeEvalCount > 0 ? totalInterventions / bridgeEvalCount : 0;
205
224
  return { stability, volatility, totalInterventions, evalCount: bridgeEvalCount };
206
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
+ }
207
274
  function synthesizeSessionReport() {
208
275
  // Gather all sessions to report on (history + current if it has data)
209
276
  const allSessions = [
@@ -554,24 +621,47 @@ function startInteractiveServer(port, onReady) {
554
621
  try {
555
622
  const body = await readBody(req);
556
623
  const payload = JSON.parse(body);
557
- if (!payload.name || !payload.baseWorld) {
558
- jsonResponse(res, 400, { error: "name and baseWorld are required" });
624
+ if (!payload.name) {
625
+ jsonResponse(res, 400, { error: "name is required" });
559
626
  return;
560
627
  }
561
628
  const id = slugify(payload.name);
562
- const variant = {
563
- id,
564
- name: payload.name,
565
- description: payload.description ?? "",
566
- baseWorld: payload.baseWorld,
567
- stateOverrides: payload.stateOverrides ?? {},
568
- events: payload.events ?? [],
569
- rounds: payload.rounds ?? 5,
570
- createdAt: new Date().toISOString(),
571
- lastResult: payload.lastResult,
572
- };
573
- const filepath = saveVariant(variant);
574
- jsonResponse(res, 200, { status: "saved", variant, filepath });
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
+ }
575
665
  }
576
666
  catch (err) {
577
667
  jsonResponse(res, 400, { error: "Invalid request body" });
@@ -689,7 +779,7 @@ function startInteractiveServer(port, onReady) {
689
779
  default_enabled: true,
690
780
  },
691
781
  ];
692
- // Social media guards for MiroFish/OASIS agent actions.
782
+ // Social media guards for OASIS agent actions.
693
783
  // These govern what AI agents can do on simulated social platforms.
694
784
  const socialGuards = [
695
785
  {
@@ -759,6 +849,8 @@ function startInteractiveServer(port, onReady) {
759
849
  immutable: false,
760
850
  intent_patterns: cg.intent_patterns,
761
851
  default_enabled: true,
852
+ ...(cg.consequence ? { consequence: cg.consequence } : {}),
853
+ ...(cg.reward ? { reward: cg.reward } : {}),
762
854
  });
763
855
  // Add patterns to vocabulary
764
856
  for (const pat of cg.intent_patterns) {
@@ -816,6 +908,90 @@ function startInteractiveServer(port, onReady) {
816
908
  });
817
909
  return;
818
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
+ }
819
995
  // Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
820
996
  // Omit `direction` — setting it enables execution-intent safety checks (prompt injection
821
997
  // detection) which falsely flag financial terms like "buy" and "sell". Bridge actions are
@@ -830,6 +1006,7 @@ function startInteractiveServer(port, onReady) {
830
1006
  actor: payload.actor,
831
1007
  action: payload.action,
832
1008
  ...(payload.payload ?? {}),
1009
+ ...(payload.state ? { state: payload.state } : {}),
833
1010
  },
834
1011
  };
835
1012
  // Evaluate directly via the governance module
@@ -848,12 +1025,77 @@ function startInteractiveServer(port, onReady) {
848
1025
  stakeholders: [{ id: payload.actor, description: payload.actor, disposition: "neutral", priorities: [] }],
849
1026
  }, nvWorld, { trace: true, level: "standard" });
850
1027
  }
851
- // Map verdict to bridge protocol
1028
+ // Map verdict to bridge protocol — includes REWARD/PENALIZE from governance engine
852
1029
  const decision = verdict.status === "BLOCK" ? "BLOCK"
853
1030
  : verdict.status === "PAUSE" ? "MODIFY"
854
- : "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;
855
1090
  // Compute live metrics from cumulative bridge evaluations
856
- const bridgeMetrics = computeBridgeMetrics(decision);
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;
857
1099
  // Broadcast governance event to connected SSE clients
858
1100
  broadcast({
859
1101
  type: "round",
@@ -863,15 +1105,19 @@ function startInteractiveServer(port, onReady) {
863
1105
  reactions: [{
864
1106
  stakeholder_id: payload.actor,
865
1107
  reaction: payload.action,
866
- impact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
867
- confidence: 0.5,
1108
+ impact: impactForDecision,
1109
+ confidence: 0.5 * agentState.rewardMultiplier,
868
1110
  trigger: "bridge",
869
- 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
+ },
870
1116
  }],
871
- avgImpact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
1117
+ avgImpact: impactForDecision,
872
1118
  maxVolatility: bridgeMetrics.volatility,
873
1119
  dynamics: [],
874
- interventionCount: decision !== "ALLOW" ? 1 : 0,
1120
+ interventionCount: effectiveDecision !== "ALLOW" && effectiveDecision !== "REWARD" ? 1 : 0,
875
1121
  });
876
1122
  // Also broadcast a stability update so the metric refreshes live
877
1123
  broadcast({
@@ -884,10 +1130,11 @@ function startInteractiveServer(port, onReady) {
884
1130
  // Record in session
885
1131
  currentSession.evaluations.push({
886
1132
  actor: payload.actor, action: payload.action,
887
- decision: decision,
1133
+ decision: effectiveDecision,
888
1134
  reason: verdict.reason ?? "", ruleId: verdict.ruleId ?? null,
889
1135
  world: payload.world ?? currentSession.world,
890
1136
  timestamp: Date.now(), payload: payload.payload,
1137
+ incentive,
891
1138
  });
892
1139
  currentSession.guardCount = customGuards.length + (nvWorld.guards?.guards?.length ?? 0);
893
1140
  // Persist to audit trail on disk
@@ -895,23 +1142,32 @@ function startInteractiveServer(port, onReady) {
895
1142
  agent: payload.actor,
896
1143
  action: payload.action,
897
1144
  actionType: payload.payload?.type ?? "unknown",
898
- verdict: decision,
1145
+ verdict: effectiveDecision,
899
1146
  reason: verdict.reason ?? "",
900
1147
  confidence: verdict.confidence ?? 0.5,
901
1148
  rulesFired: verdict.ruleId ? [{
902
1149
  id: verdict.ruleId,
903
1150
  description: verdict.reason ?? "",
904
- effect: decision === "BLOCK" ? "blocked" : decision === "MODIFY" ? "dampened" : "monitored",
905
- 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,
906
1153
  }] : [],
907
1154
  worldState: payload.world ?? currentSession.world,
908
1155
  });
909
1156
  jsonResponse(res, 200, {
910
- decision,
1157
+ decision: effectiveDecision,
911
1158
  reason: verdict.reason ?? null,
912
1159
  rule_id: verdict.ruleId ?? null,
913
1160
  evidence: verdict.evidence ?? null,
914
- 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
+ },
915
1171
  });
916
1172
  }
917
1173
  catch (err) {
@@ -1025,6 +1281,8 @@ function startInteractiveServer(port, onReady) {
1025
1281
  immutable: false,
1026
1282
  intent_patterns: rule.intent_patterns,
1027
1283
  default_enabled: true,
1284
+ ...(rule.consequence ? { consequence: rule.consequence } : {}),
1285
+ ...(rule.reward ? { reward: rule.reward } : {}),
1028
1286
  });
1029
1287
  }
1030
1288
  jsonResponse(res, 200, {
@@ -1169,6 +1427,8 @@ function startInteractiveServer(port, onReady) {
1169
1427
  const blocked = evals.filter(e => e.decision === "BLOCK").length;
1170
1428
  const modified = evals.filter(e => e.decision === "MODIFY").length;
1171
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;
1172
1432
  const uniqueActors = [...new Set(evals.map(e => e.actor))];
1173
1433
  const uniqueActions = [...new Set(evals.map(e => e.action))];
1174
1434
  const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
@@ -1182,6 +1442,8 @@ function startInteractiveServer(port, onReady) {
1182
1442
  blocked,
1183
1443
  modified,
1184
1444
  allowed,
1445
+ rewarded,
1446
+ penalized,
1185
1447
  },
1186
1448
  agents: uniqueActors,
1187
1449
  actionTypes: uniqueActions,
@@ -1388,6 +1650,308 @@ function startInteractiveServer(port, onReady) {
1388
1650
  jsonResponse(res, 200, { adapters });
1389
1651
  return;
1390
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
+ }
1391
1955
  // Run simulation via live adapter (external process)
1392
1956
  if (req.url === "/api/run-live" && req.method === "POST") {
1393
1957
  if (isRunning) {
@@ -1441,7 +2005,7 @@ function startInteractiveServer(port, onReady) {
1441
2005
  // Listen for rounds from the adapter
1442
2006
  adapter.on("round", (liveRound) => {
1443
2007
  // Build lookup of original verdicts from adapter's adaptation data.
1444
- // When an external bridge (e.g. MiroFish) already applied governance,
2008
+ // When an external bridge already applied governance,
1445
2009
  // the verdict lives in adaptation.deltas — use it instead of re-evaluating
1446
2010
  // (re-evaluating the MODIFIED action would return ALLOW, hiding the BLOCK).
1447
2011
  const adaptationByAgent = new Map();
@@ -1958,6 +2522,185 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1958
2522
  .scenario-btn { padding: 6px; }
1959
2523
  }
1960
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
+
1961
2704
  /* Rule editor */
1962
2705
  .rule-editor { margin-top: 8px; }
1963
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; }
@@ -2047,7 +2790,7 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
2047
2790
  <body>
2048
2791
  <div class="header">
2049
2792
  <div style="display:flex;align-items:center">
2050
- <h1>NV-SIM</h1>
2793
+ <h1>NeuroVerse</h1>
2051
2794
  <span class="sub">Governance Runtime</span>
2052
2795
  </div>
2053
2796
  <div class="header-right">
@@ -2059,116 +2802,58 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
2059
2802
  <div class="layout">
2060
2803
  <!-- LEFT: CONTROLS -->
2061
2804
  <div class="controls" id="controls-panel">
2062
- <!-- World Action Bar -->
2063
- <div class="world-action-bar">
2064
- <button class="btn btn-world-action" id="new-world-btn" title="Clear everything and start fresh">+ New World</button>
2065
- <button class="btn btn-world-action" id="load-file-btn" title="Load a .json world file">Load World File</button>
2066
- <button class="btn btn-world-action" id="clear-rules-btn" title="Clear custom rules only">Clear Rules</button>
2067
- <button class="btn btn-world-action btn-export" id="export-world-btn" title="Export current world as JSON">Save as World File</button>
2068
- </div>
2069
2805
 
2070
- <!-- World Source selector -->
2071
- <div class="ctrl-section">
2072
- <h3>World Source</h3>
2073
- <div class="world-source-tabs">
2074
- <label class="ws-tab active" data-source="preset">
2075
- <input type="radio" name="world-source" value="preset" checked>
2076
- <span class="ws-label">Preset</span>
2077
- <span class="ws-hint">Demo scenarios</span>
2078
- </label>
2079
- <label class="ws-tab" data-source="custom">
2080
- <input type="radio" name="world-source" value="custom">
2081
- <span class="ws-label">Custom Rules</span>
2082
- <span class="ws-hint">Define your world</span>
2083
- </label>
2084
- <label class="ws-tab" data-source="upload">
2085
- <input type="radio" name="world-source" value="upload">
2086
- <span class="ws-label">World File</span>
2087
- <span class="ws-hint">JSON / .nv-world</span>
2088
- </label>
2089
- </div>
2090
- </div>
2806
+ <!-- Start Here -->
2807
+ <button class="btn-start-here" id="start-here-btn">Start Here</button>
2091
2808
 
2092
- <!-- SOURCE: Preset -->
2093
- <div class="world-source-panel" id="source-preset">
2094
- <div class="ctrl-section">
2095
- <h3>World</h3>
2096
- <div class="ctrl-row">
2097
- <select id="world-select"></select>
2098
- </div>
2099
- <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>
2100
2814
  </div>
2101
-
2102
- <!-- State variables (dynamic sliders) -->
2103
- <div class="ctrl-section" id="state-vars-section" style="display:none">
2104
- <h3>World Rules</h3>
2105
- <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>
2106
2819
  </div>
2107
-
2108
- <!-- Scenario presets (collapsed by default) -->
2109
- <div class="ctrl-section">
2110
- <h3 style="cursor:pointer" onclick="var el=document.getElementById('scenario-list');var a=this.querySelector('.arrow');if(el.style.display==='none'){el.style.display='';a.textContent='▼'}else{el.style.display='none';a.textContent='▶'}"><span class="arrow" style="font-size:9px;margin-right:4px">▶</span>Scenarios</h3>
2111
- <div id="scenario-list" style="display:none"></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)">
2112
2824
  </div>
2113
- </div>
2114
-
2115
- <!-- SOURCE: Custom Rules (Define Your World) -->
2116
- <div class="world-source-panel" id="source-custom" style="display:none">
2117
- <div class="ctrl-section">
2118
- <h3>Define Your World</h3>
2119
- <div class="custom-world-header">
2120
- <input type="text" class="world-name-input" id="custom-world-name" placeholder="World name (e.g. Marketing Governance)">
2121
- <textarea class="world-thesis-input" id="custom-world-thesis" placeholder="What is this world about? (thesis)"></textarea>
2122
- </div>
2123
- <div class="rule-editor">
2124
- <div class="rule-editor-label">Type your governance rules:</div>
2125
- <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>
2126
- <button class="btn btn-generate-world" id="parse-rules-btn">Generate World</button>
2127
- <div id="parsed-rules" class="parsed-rules"></div>
2128
- <div id="rule-status" class="rule-status"></div>
2129
- <div class="rule-examples">
2130
- Rule patterns:<br>
2131
- <code>Block [action]</code> — hard suppression<br>
2132
- <code>Limit [X] to [N]</code> — cap extremes<br>
2133
- <code>Require [X] for [Y]</code> — structural constraint<br>
2134
- <code>Pause [X] for review</code> — human-in-the-loop<br>
2135
- <code>Allow [X]</code> — explicit permission<br>
2136
- <code>Monitor [X]</code> — circuit breaker gate
2137
- </div>
2138
- </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>
2139
2828
  </div>
2829
+ </div>
2140
2830
 
2141
- <!-- Base world (optional) -->
2142
- <div class="ctrl-section">
2143
- <h3>Base World (Optional)</h3>
2144
- <div class="ctrl-row">
2145
- <select id="custom-base-world">
2146
- <option value="">None — start from scratch</option>
2147
- </select>
2148
- <div style="font-size:10px;color:var(--text-faint);margin-top:4px">Layer your rules on top of a preset world</div>
2149
- </div>
2150
- </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>
2151
2838
  </div>
2152
2839
 
2153
- <!-- SOURCE: Upload World File -->
2154
- <div class="world-source-panel" id="source-upload" style="display:none">
2155
- <div class="ctrl-section">
2156
- <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">
2157
2846
  <div class="upload-zone" id="upload-zone">
2158
- <div class="upload-icon">&#x1F4C4;</div>
2159
2847
  <div class="upload-label">Drop a .json or .nv-world file here</div>
2160
2848
  <div class="upload-or">or</div>
2161
2849
  <button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
2162
2850
  <input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
2163
2851
  </div>
2164
- <div class="upload-paste-section">
2165
- <div class="rule-editor-label">Or paste world JSON:</div>
2166
- <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>
2167
2854
  </div>
2168
- <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>
2169
2856
  <div id="upload-status" class="rule-status"></div>
2170
-
2171
- <!-- Loaded world info -->
2172
2857
  <div id="loaded-world-info" style="display:none">
2173
2858
  <div class="loaded-world-card">
2174
2859
  <div class="lw-name" id="lw-name"></div>
@@ -2176,65 +2861,54 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
2176
2861
  <div class="lw-stats" id="lw-stats"></div>
2177
2862
  </div>
2178
2863
  </div>
2179
- </div>
2180
-
2181
- <!-- World file schema reference -->
2182
- <div class="ctrl-section">
2183
- <h3>World File Schema</h3>
2184
- <div class="schema-ref">
2185
- <div class="schema-item"><code>name</code> — world name</div>
2186
- <div class="schema-item"><code>thesis</code> — what this world is about</div>
2187
- <div class="schema-item"><code>rules[]</code> — governance rules (plain English or structured)</div>
2188
- <div class="schema-item"><code>invariants[]</code> — rules that always hold <code>{id, description}</code></div>
2189
- <div class="schema-item"><code>gates[]</code> — viability thresholds <code>{id, label, condition, severity}</code></div>
2190
- <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>
2191
2866
  </div>
2192
2867
  </div>
2193
2868
  </div>
2194
2869
 
2195
- <!-- Simulation Engine (demoted, below world source) -->
2196
- <div class="ctrl-section" style="margin-top:8px">
2197
- <h3>Engine</h3>
2198
- <div class="ctrl-row">
2199
- <select id="engine-select">
2200
- <option value="nv-sim" selected>NV-SIM (Built-in)</option>
2201
- </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>
2202
2886
  </div>
2203
- <div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
2204
2887
  </div>
2205
2888
 
2206
- <!-- Narrative injection -->
2207
- <div class="ctrl-section">
2208
- <h3>Narrative Events</h3>
2209
- <div class="inject-row">
2210
- <select id="event-select"></select>
2211
- <input type="number" id="event-round" min="1" max="20" value="3" placeholder="R">
2212
- </div>
2213
- <button class="btn btn-add" id="add-event-btn">+ Add Event</button>
2214
- <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>
2215
2893
  </div>
2216
2894
 
2217
- <!-- Rounds -->
2218
- <div class="ctrl-section">
2219
- <h3>Simulation</h3>
2220
- <div class="ctrl-row">
2221
- <div class="ctrl-label">
2222
- <span>Rounds</span>
2223
- <span class="val" id="rounds-val">5</span>
2224
- </div>
2225
- <input type="range" id="rounds-slider" min="3" max="12" value="5">
2226
- </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>
2227
2901
  </div>
2228
2902
 
2229
2903
  <!-- Run button -->
2230
- <button class="btn btn-run" id="run-btn">Run Simulation</button>
2904
+ <button class="btn btn-run" id="run-btn">Run Governance</button>
2231
2905
 
2232
- <!-- Save as variant -->
2906
+ <!-- Save as Simulation Rules -->
2233
2907
  <div id="save-section" style="margin-top:12px">
2234
- <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>
2235
2909
  <div id="save-form" style="display:none;margin-top:8px">
2236
- <input type="text" id="variant-name" placeholder="Variant name (e.g. Hormuz Closed + 3x Leverage)" class="save-input">
2237
- <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">
2238
2912
  <div style="display:flex;gap:6px;margin-top:6px">
2239
2913
  <button class="btn btn-confirm" id="confirm-save-btn">Save</button>
2240
2914
  <button class="btn btn-cancel" id="cancel-save-btn">Cancel</button>
@@ -2242,218 +2916,28 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
2242
2916
  </div>
2243
2917
  </div>
2244
2918
 
2245
- <!-- Saved variants -->
2919
+ <!-- Saved Simulation Rules -->
2246
2920
  <div class="ctrl-section" style="margin-top:16px">
2247
- <h3>Your Worlds</h3>
2248
- <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>
2249
2923
  </div>
2250
2924
 
2251
- <!-- Integration Quick-Start -->
2252
- <div class="ctrl-section" style="margin-top:16px">
2253
- <h3>Govern Any Agent System</h3>
2254
-
2255
- <div class="arch-callout">
2256
- <div class="arch-title">Pre-Execution Enforcement</div>
2257
- <div class="arch-flow">
2258
- Agent decides → <span class="arch-yes">Governance evaluates</span> → <span class="arch-yes">THEN executes</span>
2259
- </div>
2260
- <div class="arch-warn">Governance MUST happen BEFORE execution. Post-execution = audit only, not control.</div>
2261
- </div>
2262
-
2263
- <div class="fw-tabs">
2264
- <button class="fw-tab active" onclick="switchFw('generic',this)">Generic<span class="fw-sub">any agent</span></button>
2265
- <button class="fw-tab" onclick="switchFw('scienceclaw',this)">ScienceClaw<span class="fw-sub">research</span></button>
2266
- <button class="fw-tab" onclick="switchFw('mirofish',this)">MiroFish<span class="fw-sub">OASIS</span></button>
2267
- <button class="fw-tab" onclick="switchFw('langchain',this)">LangChain<span class="fw-sub">tools</span></button>
2268
- </div>
2269
-
2270
- <div class="fw-panel active" id="fw-generic">
2271
- <div class="integrate-section">
2272
- <h4>Insert before your agent executes</h4>
2273
- <div class="lang-tabs">
2274
- <button class="lang-tab active" onclick="switchLang('generic-py',this)">Python</button>
2275
- <button class="lang-tab" onclick="switchLang('generic-js',this)">JavaScript</button>
2276
- <button class="lang-tab" onclick="switchLang('generic-curl',this)">cURL</button>
2277
- </div>
2278
- <div class="lang-code-panel active" id="lang-generic-py"><div class="integrate-code"><span class="kw">import</span> requests
2279
-
2280
- <span class="kw">for</span> agent <span class="kw">in</span> agents:
2281
- raw_action = agent.decide()
2282
-
2283
- <span class="kw">try</span>:
2284
- verdict = requests.post(
2285
- <span class="str">"http://localhost:3456/api/evaluate"</span>,
2286
- json={<span class="str">"actor"</span>: agent.id,
2287
- <span class="str">"action"</span>: raw_action,
2288
- <span class="str">"world"</span>: <span class="str">"trading"</span>},
2289
- timeout=0.5
2290
- ).json()
2291
- <span class="kw">except</span>:
2292
- verdict = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
2293
-
2294
- <span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
2295
- <span class="kw">continue</span> <span class="comment"># skip this action entirely</span>
2296
- <span class="kw">elif</span> verdict[<span class="str">"decision"</span>] == <span class="str">"MODIFY"</span>:
2297
- raw_action = verdict[<span class="str">"modified_action"</span>]
2298
-
2299
- environment.apply(agent, raw_action)</div></div>
2300
- <div class="lang-code-panel" id="lang-generic-js"><div class="integrate-code"><span class="kw">for</span> (<span class="kw">const</span> agent <span class="kw">of</span> agents) {
2301
- <span class="kw">let</span> action = agent.decide()
2302
-
2303
- <span class="kw">const</span> verdict = <span class="kw">await</span> fetch(
2304
- <span class="str">"http://localhost:3456/api/evaluate"</span>, {
2305
- method: <span class="str">"POST"</span>,
2306
- headers: {<span class="str">"Content-Type"</span>: <span class="str">"application/json"</span>},
2307
- body: JSON.stringify({
2308
- actor: agent.id, action, world: <span class="str">"trading"</span>
2309
- })
2310
- }).then(r => r.json())
2311
-
2312
- <span class="kw">if</span> (verdict.decision === <span class="str">"BLOCK"</span>) <span class="kw">continue</span>
2313
- <span class="kw">if</span> (verdict.decision === <span class="str">"MODIFY"</span>)
2314
- action = verdict.modified_action
2315
-
2316
- environment.apply(agent, action)
2317
- }</div></div>
2318
- <div class="lang-code-panel" id="lang-generic-curl"><div class="integrate-code">curl -X POST http://localhost:3456/api/evaluate \
2319
- -H <span class="str">"Content-Type: application/json"</span> \
2320
- -d <span class="str">'{"actor":"agent_1","action":"panic_sell"}'</span>
2321
-
2322
- <span class="comment"># Returns: {"decision":"BLOCK","reason":"..."}</span>
2323
- <span class="comment"># Watch it appear in the dashboard →</span></div></div>
2324
- </div>
2325
- </div>
2326
-
2327
- <div class="fw-panel" id="fw-scienceclaw">
2328
- <div class="integrate-section">
2329
- <h4>Detect action type, then evaluate</h4>
2330
- <div style="font-size:10px;color:var(--yellow);margin-bottom:6px;line-height:1.4">
2331
- Don't hardcode <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">action = "post"</code> — detect intent from agent output so rules can differentiate.
2332
- </div>
2333
- <div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate, detect_action_type
2334
-
2335
- <span class="comment"># Step 1: Agent generates content</span>
2336
- results = generator.run_pubmed_search(topic)
2337
- output = generator.generate_content(topic, results)
2338
-
2339
- <span class="comment"># Step 2: Detect what kind of action this is</span>
2340
- action = detect_action_type(output[<span class="str">"content"</span>])
2341
- <span class="comment"># → "analyze", "publish", "cite", "recommend"</span>
2342
-
2343
- <span class="comment"># Step 3: Evaluate BEFORE executing</span>
2344
- verdict = evaluate(
2345
- actor=<span class="str">"Harry"</span>,
2346
- action=action,
2347
- world=<span class="str">"research"</span>
2348
- )
2349
-
2350
- <span class="comment"># Step 4: Only execute if allowed</span>
2351
- <span class="kw">if</span> verdict[<span class="str">"decision"</span>] != <span class="str">"BLOCK"</span>:
2352
- generator.post_to_infinite(output)</div>
2353
- <div style="font-size:9px;color:var(--text-muted);margin-top:6px;line-height:1.5">
2354
- See <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">bridge/scienceclaw_governed.py</code> for full example
2355
- </div>
2356
- </div>
2357
- </div>
2358
-
2359
- <div class="fw-panel" id="fw-mirofish">
2360
- <div class="integrate-section">
2361
- <h4>Replace the dict comprehension</h4>
2362
- <div style="font-size:10px;color:var(--red);margin-bottom:6px;line-height:1.4">
2363
- MiroFish executes inside <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">env.step()</code> — governance MUST happen before that call.
2364
- </div>
2365
- <div class="integrate-code" style="margin-bottom:6px;border-left:3px solid var(--red);opacity:0.6"><span class="comment"># ❌ BEFORE (opaque — no governance possible)</span>
2366
- actions = {agent: LLMAction()
2367
- <span class="kw">for</span> _, agent <span class="kw">in</span> active_agents}
2368
- <span class="kw">await</span> result.env.step(actions)</div>
2369
- <div class="integrate-code" style="border-left:3px solid var(--green)"><span class="comment"># ✅ AFTER (expand the loop, insert governance)</span>
2370
- <span class="kw">import</span> requests
2371
-
2372
- actions = {}
2373
- <span class="kw">for</span> _, agent <span class="kw">in</span> active_agents:
2374
- raw_action = LLMAction()
2375
-
2376
- <span class="kw">try</span>:
2377
- verdict = requests.post(
2378
- <span class="str">"http://localhost:3456/api/evaluate"</span>,
2379
- json={
2380
- <span class="str">"actor"</span>: getattr(agent, <span class="str">"id"</span>, str(agent)),
2381
- <span class="str">"action"</span>: str(raw_action),
2382
- <span class="str">"world"</span>: <span class="str">"social_media"</span>
2383
- },
2384
- timeout=0.5
2385
- ).json()
2386
- <span class="kw">except</span>:
2387
- verdict = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
2388
-
2389
- <span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
2390
- <span class="kw">continue</span>
2391
- <span class="kw">elif</span> verdict[<span class="str">"decision"</span>] == <span class="str">"MODIFY"</span>:
2392
- raw_action = verdict.get(<span class="str">"modified_action"</span>, raw_action)
2393
-
2394
- actions[agent] = raw_action
2395
-
2396
- <span class="kw">await</span> result.env.step(actions)</div>
2397
- <div style="font-size:9px;color:var(--text-muted);margin-top:6px;line-height:1.5">
2398
- Apply to all 3 scripts: <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_twitter_simulation.py</code>
2399
- <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_reddit_simulation.py</code>
2400
- <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_parallel_simulation.py</code>
2401
- </div>
2402
- </div>
2403
- </div>
2404
-
2405
- <div class="fw-panel" id="fw-langchain">
2406
- <div class="integrate-section">
2407
- <h4>Wrap your tool or agent executor</h4>
2408
- <div class="integrate-code"><span class="kw">import</span> requests
2409
- <span class="kw">from</span> langchain.tools <span class="kw">import</span> tool
2410
-
2411
- <span class="kw">def</span> governed(fn, world=<span class="str">"trading"</span>):
2412
- <span class="kw">def</span> wrapper(*args, **kwargs):
2413
- action = fn.__name__
2414
- <span class="kw">try</span>:
2415
- v = requests.post(
2416
- <span class="str">"http://localhost:3456/api/evaluate"</span>,
2417
- json={<span class="str">"actor"</span>: <span class="str">"langchain"</span>,
2418
- <span class="str">"action"</span>: action,
2419
- <span class="str">"world"</span>: world},
2420
- timeout=0.5
2421
- ).json()
2422
- <span class="kw">except</span>:
2423
- v = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
2424
- <span class="kw">if</span> v[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
2425
- <span class="kw">return</span> f<span class="str">"Blocked: {v['reason']}"</span>
2426
- <span class="kw">return</span> fn(*args, **kwargs)
2427
- <span class="kw">return</span> wrapper
2428
-
2429
- <span class="comment"># Usage: wrap any tool</span>
2430
- @tool
2431
- @governed
2432
- <span class="kw">def</span> execute_trade(ticker, amount):
2433
- ...</div>
2434
- </div>
2435
- </div>
2436
-
2437
- <div style="margin-top:8px">
2438
- <div class="integrate-hint">
2439
- <span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> stopped
2440
- <span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> adjusted
2441
- <span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
2442
- </div>
2443
- <div style="font-size:10px;color:var(--text-faint);margin-top:8px;border-top:1px solid var(--border);padding-top:6px">
2444
- The world file controls everything. Same rules govern simulated and live systems.
2445
- </div>
2446
- </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>
2447
2929
  </div>
2448
2930
 
2449
2931
  <!-- Session Report Panel -->
2450
2932
  <div class="ctrl-section" id="session-panel">
2451
2933
  <h3 class="ctrl-label">SESSION</h3>
2452
- <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">
2453
2935
  <div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
2454
2936
  <div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
2455
2937
  <div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
2456
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>
2457
2941
  </div>
2458
2942
  <div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
2459
2943
  <div style="display:flex;gap:6px;flex-wrap:wrap">
@@ -2466,52 +2950,58 @@ actions = {}
2466
2950
  </div>
2467
2951
  </div>
2468
2952
 
2469
- <!-- RIGHT: VIEWER -->
2953
+ <!-- RIGHT: OBSERVATION DECK -->
2470
2954
  <div class="viewer">
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>
2471
2959
 
2472
- <!-- LAYER 1: OUTCOME -->
2473
- <div class="outcome-panel" id="outcome-panel">
2474
- <div class="outcome-statement" id="outcome-statement">
2475
- <span class="outcome-empty">Run a simulation or connect an agent to see outcomes</span>
2476
- </div>
2477
- <div class="confidence-grid" id="confidence-grid" style="display:none">
2478
- <div class="confidence-card">
2479
- <div class="cc-label">Conclusion Strength</div>
2480
- <div class="cc-value" id="cc-strength">--</div>
2481
- </div>
2482
- <div class="confidence-card">
2483
- <div class="cc-label">Evidence Quality</div>
2484
- <div class="cc-value" id="cc-evidence">--</div>
2485
- </div>
2486
- <div class="confidence-card">
2487
- <div class="cc-label">Risk of Error</div>
2488
- <div class="cc-value" id="cc-risk">--</div>
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>
2489
2968
  </div>
2490
2969
  </div>
2491
- <div class="outcome-context" id="outcome-context"></div>
2970
+ <div class="choice-round-label" id="choice-round-label" style="display:none">Round <span id="choice-round-num">1</span></div>
2492
2971
  </div>
2493
2972
 
2494
- <!-- LAYER 2: BEHAVIOR -->
2495
- <div class="behavior-panel" id="behavior-panel-v2">
2496
- <h2>What Agents Did</h2>
2497
- <div class="behavior-shifts" id="behavior-shifts">
2498
- <div class="behavior-empty">Waiting for agent actions</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>
2499
2978
  </div>
2500
- <div class="activity-toggle" id="activity-toggle" onclick="document.getElementById('activity-timeline').classList.toggle('open');this.querySelector('.arrow').textContent=document.getElementById('activity-timeline').classList.contains('open')?'▼':'▶'" style="display:none">
2501
- <span class="arrow">▶</span> See activity timeline
2979
+ <div class="choice-stream" id="choice-stream">
2980
+ <div class="stream-empty">Agent decisions will stream here in real time</div>
2502
2981
  </div>
2503
- <div class="activity-timeline" id="activity-timeline"></div>
2504
2982
  </div>
2505
2983
 
2506
- <!-- LAYER 3: WHY -->
2507
- <div class="why-panel" id="why-panel">
2508
- <h2>Why This Happened</h2>
2509
- <div id="why-content">
2510
- <div class="why-empty">Causation analysis appears after agent actions are evaluated</div>
2511
- </div>
2984
+ <!-- AFTER: Behavioral Drift — first 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>
2512
2996
  </div>
2513
2997
 
2514
- <!-- LAYER 4: EXPORT -->
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 -->
2515
3005
  <div class="export-bar">
2516
3006
  <button class="export-btn" onclick="exportPDF()">Download PDF</button>
2517
3007
  <button class="export-btn" onclick="exportCSV()">Export CSV</button>
@@ -2555,27 +3045,65 @@ actions = {}
2555
3045
  </div>
2556
3046
  </div>
2557
3047
 
2558
- <script>
2559
- // ============================================
2560
- // LANGUAGE & FRAMEWORK TABS
2561
- // ============================================
2562
- function switchLang(lang, btn) {
2563
- const container = btn.closest('.integrate-section') || document;
2564
- container.querySelectorAll('.lang-tab').forEach(t => t.classList.remove('active'));
2565
- container.querySelectorAll('.lang-code-panel').forEach(p => p.classList.remove('active'));
2566
- btn.classList.add('active');
2567
- const panel = document.getElementById('lang-' + lang);
2568
- if (panel) panel.classList.add('active');
2569
- }
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">
2570
3058
 
2571
- function switchFw(fw, btn) {
2572
- document.querySelectorAll('.fw-tab').forEach(t => t.classList.remove('active'));
2573
- document.querySelectorAll('.fw-panel').forEach(p => p.classList.remove('active'));
2574
- btn.classList.add('active');
2575
- const panel = document.getElementById('fw-' + fw);
2576
- if (panel) panel.classList.add('active');
2577
- }
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>
3064
+ </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.
3072
+ </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>
3080
+ </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>
3088
+ </div>
3089
+ </div>
2578
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>
3097
+ </div>
3098
+ </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>
3102
+ </div>
3103
+ </div>
3104
+ </div>
3105
+
3106
+ <script>
2579
3107
  // ============================================
2580
3108
  // OUTCOME INTELLIGENCE LAYER
2581
3109
  // ============================================
@@ -2659,6 +3187,12 @@ function translateBehaviorNarrative(reaction, verdict) {
2659
3187
  if (status === 'BLOCK') {
2660
3188
  return 'abandoned ' + action + ' and switched to a safer strategy';
2661
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
+ }
2662
3196
  if (status === 'MODIFY' || status === 'PAUSE') {
2663
3197
  return 'scaled back ' + action + ' after early resistance';
2664
3198
  }
@@ -2692,7 +3226,9 @@ function generateBehaviorShifts(shiftData, bLog) {
2692
3226
  if (sentences.length === 0 && bLog.length > 0) {
2693
3227
  var blocked = bLog.filter(function(e) { return e.status === 'BLOCK'; }).length;
2694
3228
  var modified = bLog.filter(function(e) { return e.status === 'MODIFY' || e.status === 'PAUSE'; }).length;
2695
- var proceeded = bLog.length - blocked - modified;
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;
2696
3232
 
2697
3233
  if (blocked > 0) {
2698
3234
  var bPct = Math.round((blocked / bLog.length) * 100);
@@ -2702,7 +3238,15 @@ function generateBehaviorShifts(shiftData, bLog) {
2702
3238
  var mPct = Math.round((modified / bLog.length) * 100);
2703
3239
  sentences.push(mPct + '% of agents (' + modified + ') reduced position size after initial attempts failed');
2704
3240
  }
2705
- if (proceeded > 0 && (blocked > 0 || modified > 0)) {
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)) {
2706
3250
  var pPct = Math.round((proceeded / bLog.length) * 100);
2707
3251
  sentences.push(pPct + '% of agents (' + proceeded + ') maintained their original strategy throughout');
2708
3252
  }
@@ -2786,22 +3330,19 @@ let behaviorLog = []; // { agent, action, status, reason, ts }
2786
3330
  let latestStability = 0;
2787
3331
  let latestVolatility = 0;
2788
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: {...} }
3338
+
2789
3339
  const statusEl = document.getElementById('status');
2790
- const worldSelect = document.getElementById('world-select');
2791
3340
  const stateVarsSection = document.getElementById('state-vars-section');
2792
3341
  const stateVarsEl = document.getElementById('state-vars');
2793
- const scenarioListEl = document.getElementById('scenario-list');
2794
- const eventSelect = document.getElementById('event-select');
2795
- const eventRoundInput = document.getElementById('event-round');
2796
- const injectListEl = document.getElementById('inject-list');
2797
- const roundsSlider = document.getElementById('rounds-slider');
2798
- const roundsVal = document.getElementById('rounds-val');
2799
3342
  const runBtn = document.getElementById('run-btn');
2800
3343
  const agentsEl = document.getElementById('agents');
2801
3344
  const logEl = document.getElementById('log');
2802
3345
  const activeInvEl = document.getElementById('active-invariants');
2803
- const engineSelect = document.getElementById('engine-select');
2804
- const engineStatusEl = document.getElementById('engine-status');
2805
3346
  const traceSourceEl = document.getElementById('trace-source');
2806
3347
  const outcomeStatementEl = document.getElementById('outcome-statement');
2807
3348
  const confidenceGridEl = document.getElementById('confidence-grid');
@@ -2861,79 +3402,50 @@ function exportCSV() {
2861
3402
  }
2862
3403
 
2863
3404
  function copyShareSummary() {
2864
- var outcome = outcomeStatementEl.textContent;
2865
- var shifts = [];
2866
- behaviorShiftsEl.querySelectorAll('.behavior-shift-item').forEach(function(el) { shifts.push('- ' + el.textContent); });
2867
- var causes = [];
2868
- whyContentEl.querySelectorAll('.why-item').forEach(function(el) { causes.push('- ' + el.textContent); });
3405
+ var text = 'OBSERVATION DECK SUMMARY\\n\\n';
2869
3406
 
2870
- var text = 'OUTCOME: ' + outcome + '\\n\\n';
2871
- if (shifts.length) text += 'BEHAVIOR:\\n' + shifts.join('\\n') + '\\n\\n';
2872
- if (causes.length) text += 'WHY:\\n' + causes.join('\\n') + '\\n';
2873
- text += '\\nGenerated by NeuroVerse Simulations';
3407
+ // Drift verdict
3408
+ var driftVerdict = document.getElementById('drift-verdict');
3409
+ if (driftVerdict && driftVerdict.textContent) {
3410
+ text += 'BEHAVIORAL DRIFT: ' + driftVerdict.textContent + '\\n\\n';
3411
+ }
2874
3412
 
2875
- navigator.clipboard.writeText(text).then(function() {
2876
- alert('Summary copied to clipboard');
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');
2877
3426
  });
2878
3427
  }
2879
3428
 
2880
3429
  // ============================================
2881
- // INIT — Load worlds, scenarios, narratives, adapters
3430
+ // INIT — Load bridge capabilities and saved rules
2882
3431
  // ============================================
2883
3432
  async function init() {
2884
- const [wRes, sRes, nRes, aRes] = await Promise.all([
2885
- fetch('/api/worlds').then(r => r.json()),
2886
- fetch('/api/scenarios').then(r => r.json()),
2887
- fetch('/api/narratives').then(r => r.json()),
2888
- 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: [] })),
2889
3437
  ]);
2890
3438
 
2891
3439
  worlds = wRes.worlds;
2892
- scenarios = sRes.scenarios;
2893
- narratives = nRes.narratives;
2894
-
2895
- // Populate engine selector with live adapters
2896
- (aRes.adapters || []).forEach(function(a) {
2897
- const opt = document.createElement('option');
2898
- opt.value = a.id;
2899
- opt.textContent = a.label;
2900
- engineSelect.appendChild(opt);
2901
- });
2902
3440
 
2903
- // Populate world select
2904
- worlds.forEach(w => {
2905
- const opt = document.createElement('option');
2906
- opt.value = w.id;
2907
- opt.textContent = w.title;
2908
- worldSelect.appendChild(opt);
3441
+ // Index bridge capabilities
3442
+ (bcRes.bridges || []).forEach(function(b) {
3443
+ bridgeCapabilities[b.id] = b;
2909
3444
  });
2910
3445
 
2911
- // Populate event select
2912
- Object.entries(narratives).forEach(([id, ev]) => {
2913
- const opt = document.createElement('option');
2914
- opt.value = id;
2915
- opt.textContent = ev.headline.slice(0, 40);
2916
- eventSelect.appendChild(opt);
2917
- });
2918
-
2919
- // Populate scenario presets
2920
- Object.entries(scenarios).forEach(([id, s]) => {
2921
- const btn = document.createElement('button');
2922
- btn.className = 'scenario-btn';
2923
- btn.innerHTML = '<div class="stitle">' + s.title + '</div><div class="sdesc">' + s.description + '</div>';
2924
- btn.onclick = () => loadScenario(id, s);
2925
- scenarioListEl.appendChild(btn);
2926
- });
2927
-
2928
- // Select first world
2929
- if (worlds.length > 0) selectWorld(worlds[0].id);
2930
-
2931
- // Load saved variants
3446
+ // Load saved rules
2932
3447
  await loadVariants();
2933
3448
 
2934
- // Populate base world selector for custom rules mode
2935
- populateBaseWorldSelect();
2936
-
2937
3449
  // Connect SSE
2938
3450
  connectSSE();
2939
3451
  }
@@ -2942,110 +3454,384 @@ function selectWorld(worldId) {
2942
3454
  currentWorld = worlds.find(w => w.id === worldId);
2943
3455
  if (!currentWorld) return;
2944
3456
 
2945
- worldSelect.value = worldId;
2946
- document.getElementById('world-thesis').textContent = '"' + currentWorld.thesis + '"';
3457
+ // Render state variable controls if world has them
3458
+ renderStateVars();
3459
+ }
2947
3460
 
2948
- // Render state variable controls
2949
- if (currentWorld.stateVariables && currentWorld.stateVariables.length > 0) {
2950
- stateVarsSection.style.display = '';
2951
- stateVarsEl.innerHTML = '';
2952
- currentWorld.stateVariables.forEach(sv => {
2953
- const row = document.createElement('div');
2954
- row.className = 'ctrl-row';
2955
-
2956
- if (sv.type === 'number' && sv.range) {
2957
- const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
2958
- row.innerHTML =
2959
- '<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
2960
- '<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 + '">';
2961
- stateVarsEl.appendChild(row);
2962
- const slider = row.querySelector('input');
2963
- slider.addEventListener('input', () => {
2964
- document.getElementById('sv-val-' + sv.id).textContent = slider.value;
2965
- });
2966
- } else if (sv.type === 'enum' && sv.enum_values) {
2967
- row.innerHTML =
2968
- '<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
2969
- '<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
2970
- sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
2971
- '</select>';
2972
- stateVarsEl.appendChild(row);
2973
- } else if (sv.type === 'boolean') {
2974
- row.innerHTML =
2975
- '<div class="toggle-row">' +
2976
- '<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
2977
- '<span class="toggle-label">' + sv.label + '</span>' +
2978
- '</div>';
2979
- stateVarsEl.appendChild(row);
2980
- const toggle = row.querySelector('.toggle');
2981
- toggle.addEventListener('click', () => {
2982
- toggle.classList.toggle('on');
2983
- });
2984
- }
2985
- });
2986
- } else {
3461
+ function renderStateVars() {
3462
+ if (!currentWorld || !currentWorld.stateVariables || currentWorld.stateVariables.length === 0) {
2987
3463
  stateVarsSection.style.display = 'none';
3464
+ return;
2988
3465
  }
2989
-
2990
- // Show invariants
2991
- activeInvEl.innerHTML = currentWorld.invariants.map(inv =>
2992
- '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
2993
- ).join('') + (currentWorld.gates || []).map(g =>
2994
- '<div class="inv-item" style="color:' + (g.severity === 'critical' ? '#f87171' : '#fbbf24') + '">[' + g.id + '] ' + g.label + '</div>'
2995
- ).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
+ });
2996
3504
  }
2997
3505
 
2998
- function loadScenario(id, scenario) {
2999
- // Set world
3000
- selectWorld(scenario.world);
3001
- // Set events
3002
- injectedEvents = scenario.events.slice();
3003
- renderInjectedEvents();
3004
- // Set rounds
3005
- const r = scenario.rounds || 5;
3006
- roundsSlider.value = Math.min(r, 12);
3007
- roundsVal.textContent = Math.min(r, 12);
3008
- }
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
+ });
3009
3518
 
3010
3519
  // ============================================
3011
- // NARRATIVE EVENT INJECTION
3520
+ // THESIS INPUT — Generate Governance Rules
3012
3521
  // ============================================
3013
- document.getElementById('add-event-btn').addEventListener('click', () => {
3014
- const eventId = eventSelect.value;
3015
- const round = parseInt(eventRoundInput.value);
3016
- if (!eventId || isNaN(round)) return;
3017
- injectedEvents.push(eventId + '@' + round);
3018
- renderInjectedEvents();
3019
- });
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;
3020
3530
 
3021
- function renderInjectedEvents() {
3022
- injectListEl.innerHTML = injectedEvents.map((ev, i) =>
3023
- '<div class="inject-item"><span>' + ev + '</span><span class="remove" data-idx="' + i + '">x</span></div>'
3024
- ).join('');
3025
- injectListEl.querySelectorAll('.remove').forEach(el => {
3026
- el.addEventListener('click', () => {
3027
- injectedEvents.splice(parseInt(el.dataset.idx), 1);
3028
- renderInjectedEvents();
3531
+ currentThesis = thesis;
3532
+ generateRulesBtn.disabled = true;
3533
+ generateRulesBtn.textContent = 'Generating...';
3534
+ ruleGenStatusEl.textContent = '';
3535
+
3536
+ try {
3537
+ const resp = await fetch('/api/generate-rules', {
3538
+ method: 'POST',
3539
+ headers: { 'Content-Type': 'application/json' },
3540
+ body: JSON.stringify({ thesis }),
3029
3541
  });
3030
- });
3031
- }
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
+ });
3588
+
3589
+ // ============================================
3590
+ // WORLD FILE TOGGLE
3591
+ // ============================================
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
+ });
3032
3603
 
3033
3604
  // ============================================
3034
- // WORLD SELECT
3605
+ // BRIDGE CONNECTION
3035
3606
  // ============================================
3036
- worldSelect.addEventListener('change', () => selectWorld(worldSelect.value));
3037
- roundsSlider.addEventListener('input', () => { roundsVal.textContent = roundsSlider.value; });
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
+ }
3038
3818
 
3039
3819
  // ============================================
3040
- // RUN SIMULATION
3820
+ // RUN GOVERNANCE
3041
3821
  // ============================================
3042
3822
  runBtn.addEventListener('click', async () => {
3043
- 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
+
3044
3829
  runBtn.disabled = true;
3045
3830
  runBtn.textContent = 'Running...';
3046
3831
 
3047
3832
  // Reset viewer state
3048
3833
  totalInterventions = 0;
3834
+ totalActions = 0;
3049
3835
  agentsEl.innerHTML = '';
3050
3836
  logEl.innerHTML = '';
3051
3837
  document.getElementById('m-stability').textContent = '--';
@@ -3053,9 +3839,9 @@ runBtn.addEventListener('click', async () => {
3053
3839
  document.getElementById('m-round').textContent = '--';
3054
3840
  document.getElementById('m-interventions').textContent = '0';
3055
3841
 
3056
- // Gather state overrides
3842
+ // Gather state overrides from sliders (if world has them)
3057
3843
  const stateOverrides = {};
3058
- if (currentWorld.stateVariables) {
3844
+ if (currentWorld && currentWorld.stateVariables) {
3059
3845
  currentWorld.stateVariables.forEach(sv => {
3060
3846
  const el = document.getElementById('sv-' + sv.id);
3061
3847
  if (!el) return;
@@ -3065,39 +3851,36 @@ runBtn.addEventListener('click', async () => {
3065
3851
  });
3066
3852
  }
3067
3853
 
3068
- const selectedEngine = engineSelect.value;
3069
-
3070
3854
  try {
3071
- if (selectedEngine === 'nv-sim') {
3072
- // Built-in simulation
3073
- const config = {
3074
- 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',
3075
3860
  stateOverrides,
3076
- injectEvents: injectedEvents.length > 0 ? injectedEvents : undefined,
3077
- rounds: parseInt(roundsSlider.value),
3078
3861
  };
3079
- await fetch('/api/run-sim', {
3862
+ await fetch('/api/run-live', {
3080
3863
  method: 'POST',
3081
3864
  headers: { 'Content-Type': 'application/json' },
3082
- body: JSON.stringify(config),
3865
+ body: JSON.stringify(payload),
3083
3866
  });
3084
3867
  } else {
3085
- // Live adapter (external simulator)
3086
- const payload = {
3087
- adapterId: selectedEngine,
3088
- worldId: currentWorld.id,
3868
+ // Fallback: run internal simulation with generated rules
3869
+ const config = {
3870
+ worldId: currentWorld ? currentWorld.id : 'social_simulation',
3089
3871
  stateOverrides,
3872
+ rounds: 5,
3090
3873
  };
3091
- await fetch('/api/run-live', {
3874
+ await fetch('/api/run-sim', {
3092
3875
  method: 'POST',
3093
3876
  headers: { 'Content-Type': 'application/json' },
3094
- body: JSON.stringify(payload),
3877
+ body: JSON.stringify(config),
3095
3878
  });
3096
3879
  }
3097
3880
  } catch (err) {
3098
- addLog('Error starting simulation: ' + err.message, 'block');
3881
+ addLog('Error starting governance: ' + err.message, 'block');
3099
3882
  runBtn.disabled = false;
3100
- runBtn.textContent = 'Run Simulation';
3883
+ runBtn.textContent = 'Run Governance';
3101
3884
  }
3102
3885
  });
3103
3886
 
@@ -3225,9 +4008,13 @@ function renderAgents(reactions) {
3225
4008
  }
3226
4009
 
3227
4010
  // ============================================
3228
- // PANEL RENDERINGOutcome / Behavior / Why
4011
+ // OBSERVATION DECKRendering
3229
4012
  // ============================================
3230
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
+
3231
4018
  function recordBehavior(reactions, round) {
3232
4019
  if (!reactions || !reactions.length) return;
3233
4020
  reactions.forEach(function(r) {
@@ -3244,62 +4031,283 @@ function recordBehavior(reactions, round) {
3244
4031
  });
3245
4032
  }
3246
4033
 
3247
- function updateOutcomePanel() {
3248
- var worldThesis = currentWorld ? (currentWorld.thesis || currentWorld.description || '') : '';
3249
- var outcome = generateOutcome(latestStability, latestVolatility, shiftTracker, worldThesis);
3250
- outcomeStatementEl.textContent = outcome;
3251
-
3252
- // Structured confidence card
3253
- var conf = computeConfidence(latestStability, latestVolatility, totalInterventions, totalActions);
3254
- confidenceGridEl.style.display = 'grid';
3255
- document.getElementById('cc-strength').textContent = conf.strength;
3256
- document.getElementById('cc-strength').className = 'cc-value ' + conf.strengthCls;
3257
- document.getElementById('cc-evidence').textContent = conf.evidence;
3258
- document.getElementById('cc-evidence').className = 'cc-value ' + conf.evidenceCls;
3259
- document.getElementById('cc-risk').textContent = conf.risk;
3260
- document.getElementById('cc-risk').className = 'cc-value ' + conf.riskCls;
3261
-
3262
- // Context line
3263
- outcomeContextEl.textContent = 'Based on ' + totalActions + ' agent action' + (totalActions !== 1 ? 's' : '') +
3264
- (shiftTracker.total > 0 ? ' across ' + (document.getElementById('m-round').textContent || '') : '');
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);
3265
4055
  }
3266
4056
 
3267
- function updateBehaviorPanel() {
3268
- // Quantified behavioral shifts in human language
3269
- var shifts = generateBehaviorShifts(shiftTracker, behaviorLog);
3270
- if (shifts.length > 0) {
3271
- var html = shifts.map(function(s) {
3272
- return '<div class="behavior-shift-item">' + s + '</div>';
3273
- }).join('');
3274
- behaviorShiftsEl.innerHTML = html;
3275
- }
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
+ }
3276
4068
 
3277
- // Activity timeline (narrative micro-events, expandable)
3278
- if (behaviorLog.length > 0) {
3279
- activityToggleEl.style.display = 'block';
3280
- var recent = behaviorLog.slice(-30).reverse();
3281
- var tHtml = recent.map(function(e) {
3282
- var narrative = translateBehaviorNarrative(e.action, { status: e.status });
3283
- return '<div class="activity-item"><span class="activity-agent">' + e.agent + '</span> ' + narrative + '</div>';
3284
- }).join('');
3285
- activityTimelineEl.innerHTML = tHtml;
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;
3286
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;
3287
4178
  }
3288
4179
 
3289
- function updateWhyPanel() {
3290
- var causes = generateCausation(shiftTracker, behaviorLog);
3291
- if (causes.length > 0) {
3292
- var html = causes.map(function(c) {
3293
- return '<div class="why-item">' + c + '</div>';
3294
- }).join('');
3295
- whyContentEl.innerHTML = html;
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) + '%)');
3296
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>';
3297
4305
  }
3298
4306
 
4307
+ // ── Master update ──
3299
4308
  function updateAllPanels() {
3300
- updateOutcomePanel();
3301
- updateBehaviorPanel();
3302
- updateWhyPanel();
4309
+ // Observation Deck updates are driven by renderChoiceBubbles/renderDecisionFlow
4310
+ // called directly from handleEvent — this function exists for compatibility
3303
4311
  }
3304
4312
 
3305
4313
  function handleEvent(event) {
@@ -3307,18 +4315,34 @@ function handleEvent(event) {
3307
4315
  statusEl.className = 'status live';
3308
4316
  statusEl.textContent = 'LIVE';
3309
4317
  // Show simulation source
3310
- const src = event.source || 'nv-sim';
3311
- if (src !== 'nv-sim') {
4318
+ const src = event.source || 'governance';
4319
+ if (src !== 'nv-sim' && src !== 'governance') {
3312
4320
  traceSourceEl.textContent = '● ' + src.toUpperCase() + ' (LIVE)';
3313
4321
  traceSourceEl.style.color = '#4ade80';
3314
- engineStatusEl.textContent = 'Streaming from ' + src;
3315
- engineStatusEl.style.color = '#4ade80';
3316
4322
  } else {
3317
4323
  traceSourceEl.textContent = '';
3318
- engineStatusEl.textContent = '';
3319
4324
  }
3320
4325
  addLog('Simulation started: ' + event.agents.length + ' agents, ' + event.totalRounds + ' rounds' + (src !== 'nv-sim' ? ' [source: ' + src + ']' : ''));
3321
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';
3322
4346
  // Store narrative events by round for trace rendering
3323
4347
  narrativeEventsByRound = {};
3324
4348
  (event.narrativeEvents || []).forEach(function(ev) {
@@ -3407,11 +4431,25 @@ function handleEvent(event) {
3407
4431
  document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
3408
4432
  document.getElementById('m-interventions').textContent = totalInterventions;
3409
4433
 
3410
- // Track system shifts (data collection, not displayed)
4434
+ // Track system shifts (data collection)
3411
4435
  trackShift(event);
3412
4436
 
3413
- // Update all visible panels
3414
- updateAllPanels();
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
+ }
3415
4453
 
3416
4454
  // Add trace entry for audit
3417
4455
  addTraceRound(event);
@@ -3436,7 +4474,12 @@ function handleEvent(event) {
3436
4474
  renderSystemShift(r);
3437
4475
  renderRuleImpacts(r);
3438
4476
  renderEnforcementClassification(r.enforcementClassification || []);
3439
- updateAllPanels();
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);
3440
4483
  lastSimResult = {
3441
4484
  stability: r.governed.metrics.stabilityScore,
3442
4485
  volatility: r.governed.metrics.maxVolatility,
@@ -3445,7 +4488,88 @@ function handleEvent(event) {
3445
4488
  };
3446
4489
  }
3447
4490
  runBtn.disabled = false;
3448
- 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('');
3449
4573
  }
3450
4574
  }
3451
4575
 
@@ -3720,11 +4844,11 @@ cancelSaveBtn.addEventListener('click', () => {
3720
4844
 
3721
4845
  confirmSaveBtn.addEventListener('click', async () => {
3722
4846
  const name = variantNameInput.value.trim();
3723
- if (!name || !currentWorld) return;
4847
+ if (!name) return;
3724
4848
 
3725
4849
  // Gather current state overrides
3726
4850
  const stateOverrides = {};
3727
- if (currentWorld.stateVariables) {
4851
+ if (currentWorld && currentWorld.stateVariables) {
3728
4852
  currentWorld.stateVariables.forEach(sv => {
3729
4853
  const el = document.getElementById('sv-' + sv.id);
3730
4854
  if (!el) return;
@@ -3734,13 +4858,17 @@ confirmSaveBtn.addEventListener('click', async () => {
3734
4858
  });
3735
4859
  }
3736
4860
 
4861
+ // Save as SimulationRules (complete snapshot)
3737
4862
  const payload = {
3738
4863
  name,
3739
4864
  description: variantDescInput.value.trim(),
3740
- 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,
3741
4871
  stateOverrides,
3742
- events: injectedEvents.slice(),
3743
- rounds: parseInt(roundsSlider.value),
3744
4872
  lastResult: lastSimResult,
3745
4873
  };
3746
4874
 
@@ -3773,18 +4901,21 @@ async function loadVariants() {
3773
4901
 
3774
4902
  function renderVariants(variants) {
3775
4903
  if (variants.length === 0) {
3776
- 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>';
3777
4905
  return;
3778
4906
  }
3779
4907
  variantListEl.innerHTML = variants.map(v => {
3780
4908
  const resultHtml = v.lastResult
3781
4909
  ? '<span class="vresult">Stability: ' + (v.lastResult.stability * 100).toFixed(0) + '% | Effectiveness: ' + (v.lastResult.governanceEffectiveness * 100).toFixed(0) + '%</span>'
3782
- : '<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>' : '');
3783
4913
  return '<div class="variant-card" data-vid="' + v.id + '">' +
3784
4914
  '<div class="vname">' + v.name + '</div>' +
4915
+ (v.thesis ? '<div class="vdesc" style="font-style:italic">' + v.thesis + '</div>' : '') +
3785
4916
  (v.description ? '<div class="vdesc">' + v.description + '</div>' : '') +
3786
- '<span class="vbase">' + v.baseWorld + '</span>' +
3787
- '<div class="vmeta">' + resultHtml + ' | ' + v.events.length + ' events | ' + v.rounds + ' rounds</div>' +
4917
+ bridgeLabel +
4918
+ '<div class="vmeta">' + resultHtml + (rulesCount ? ' | ' + rulesCount : '') + '</div>' +
3788
4919
  '<span class="vdelete" data-vid="' + v.id + '">delete</span>' +
3789
4920
  '</div>';
3790
4921
  }).join('');
@@ -3815,38 +4946,49 @@ function renderVariants(variants) {
3815
4946
  }
3816
4947
 
3817
4948
  function loadVariant(variant) {
3818
- // Set world
3819
- selectWorld(variant.baseWorld);
3820
-
3821
- // Apply state overrides
3822
- if (variant.stateOverrides && currentWorld && currentWorld.stateVariables) {
3823
- currentWorld.stateVariables.forEach(sv => {
3824
- if (sv.id in variant.stateOverrides) {
3825
- const el = document.getElementById('sv-' + sv.id);
3826
- if (!el) return;
3827
- if (sv.type === 'boolean') {
3828
- const val = variant.stateOverrides[sv.id];
3829
- if (val && !el.classList.contains('on')) el.classList.add('on');
3830
- if (!val && el.classList.contains('on')) el.classList.remove('on');
3831
- } else {
3832
- el.value = variant.stateOverrides[sv.id];
3833
- // Update value display for sliders
3834
- const valEl = document.getElementById('sv-val-' + sv.id);
3835
- if (valEl) valEl.textContent = variant.stateOverrides[sv.id];
3836
- }
3837
- }
3838
- });
3839
- }
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('');
3840
4974
 
3841
- // Set events
3842
- injectedEvents = variant.events.slice();
3843
- renderInjectedEvents();
4975
+ ruleGenStatusEl.textContent = variant.rules.length + ' rule(s) loaded from "' + variant.name + '".';
4976
+ ruleGenStatusEl.className = 'rule-status success';
4977
+ }
3844
4978
 
3845
- // Set rounds
3846
- roundsSlider.value = Math.min(variant.rounds, 12);
3847
- 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
+ }
3848
4990
 
3849
- addLog('Loaded variant: ' + variant.name, '');
4991
+ addLog('Loaded rules: ' + variant.name, '');
3850
4992
  }
3851
4993
 
3852
4994
  // ============================================
@@ -3889,129 +5031,15 @@ const savedTheme = localStorage.getItem('nv-theme');
3889
5031
  if (savedTheme) applyTheme(savedTheme);
3890
5032
 
3891
5033
  // ============================================
3892
- // WORLD SOURCE SWITCHING
5034
+ // (Old world source switching removed — replaced by thesis + bridge flow)
3893
5035
  // ============================================
3894
- let currentWorldSource = 'preset';
3895
- const worldSourceTabs = document.querySelectorAll('.ws-tab');
3896
- const sourcePresetPanel = document.getElementById('source-preset');
3897
- const sourceCustomPanel = document.getElementById('source-custom');
3898
- const sourceUploadPanel = document.getElementById('source-upload');
3899
-
3900
- worldSourceTabs.forEach(tab => {
3901
- tab.addEventListener('click', () => {
3902
- const source = tab.dataset.source;
3903
- if (source === currentWorldSource) return;
3904
-
3905
- currentWorldSource = source;
3906
-
3907
- // Update tab visuals
3908
- worldSourceTabs.forEach(t => t.classList.remove('active'));
3909
- tab.classList.add('active');
3910
- tab.querySelector('input').checked = true;
3911
-
3912
- // Show/hide panels
3913
- sourcePresetPanel.style.display = source === 'preset' ? '' : 'none';
3914
- sourceCustomPanel.style.display = source === 'custom' ? '' : 'none';
3915
- sourceUploadPanel.style.display = source === 'upload' ? '' : 'none';
3916
- });
3917
- });
3918
-
3919
- // Populate base world selector in custom rules panel
3920
- function populateBaseWorldSelect() {
3921
- const select = document.getElementById('custom-base-world');
3922
- if (!select) return;
3923
- worlds.forEach(w => {
3924
- const opt = document.createElement('option');
3925
- opt.value = w.id;
3926
- opt.textContent = w.title;
3927
- select.appendChild(opt);
3928
- });
3929
- }
3930
5036
 
3931
5037
  // ============================================
3932
- // WORLD ACTION BAR
5038
+ // (Old world action bar removed — replaced by thesis + bridge flow)
3933
5039
  // ============================================
3934
5040
 
3935
- // + New World
3936
- document.getElementById('new-world-btn').addEventListener('click', () => {
3937
- // Switch to custom rules mode
3938
- currentWorldSource = 'custom';
3939
- worldSourceTabs.forEach(t => {
3940
- t.classList.toggle('active', t.dataset.source === 'custom');
3941
- t.querySelector('input').checked = t.dataset.source === 'custom';
3942
- });
3943
- sourcePresetPanel.style.display = 'none';
3944
- sourceCustomPanel.style.display = '';
3945
- sourceUploadPanel.style.display = 'none';
3946
-
3947
- // Clear everything
3948
- document.getElementById('custom-world-name').value = '';
3949
- document.getElementById('custom-world-thesis').value = '';
3950
- document.getElementById('rule-input').value = '';
3951
- document.getElementById('parsed-rules').innerHTML = '';
3952
- document.getElementById('rule-status').textContent = '';
3953
- document.getElementById('rule-status').className = 'rule-status';
3954
- document.getElementById('custom-base-world').value = '';
3955
-
3956
- // Clear active rules server-side
3957
- fetch('/api/clear-rules', { method: 'POST' });
3958
-
3959
- // Reset right panel
3960
- document.getElementById('active-invariants').innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No rules loaded. Define your world.</div>';
3961
- });
3962
-
3963
- // Clear Rules
3964
- document.getElementById('clear-rules-btn').addEventListener('click', async () => {
3965
- await fetch('/api/clear-rules', { method: 'POST' });
3966
-
3967
- // Clear rule editor UI
3968
- const ruleInput = document.getElementById('rule-input');
3969
- if (ruleInput) ruleInput.value = '';
3970
- const parsed = document.getElementById('parsed-rules');
3971
- if (parsed) parsed.innerHTML = '';
3972
- const status = document.getElementById('rule-status');
3973
- if (status) { status.textContent = 'Rules cleared.'; status.className = 'rule-status success'; }
3974
-
3975
- // Clear upload state
3976
- const uploadStatus = document.getElementById('upload-status');
3977
- if (uploadStatus) { uploadStatus.textContent = 'Rules cleared.'; uploadStatus.className = 'rule-status success'; }
3978
- const loadedInfo = document.getElementById('loaded-world-info');
3979
- if (loadedInfo) loadedInfo.style.display = 'none';
3980
- });
3981
-
3982
- // Load World File (switch to upload tab)
3983
- document.getElementById('load-file-btn').addEventListener('click', () => {
3984
- currentWorldSource = 'upload';
3985
- worldSourceTabs.forEach(t => {
3986
- t.classList.toggle('active', t.dataset.source === 'upload');
3987
- t.querySelector('input').checked = t.dataset.source === 'upload';
3988
- });
3989
- sourcePresetPanel.style.display = 'none';
3990
- sourceCustomPanel.style.display = 'none';
3991
- sourceUploadPanel.style.display = '';
3992
- });
3993
-
3994
- // Save as World File (export)
3995
- document.getElementById('export-world-btn').addEventListener('click', async () => {
3996
- try {
3997
- const resp = await fetch('/api/export-world');
3998
- const data = await resp.json();
3999
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
4000
- const url = URL.createObjectURL(blob);
4001
- const a = document.createElement('a');
4002
- a.href = url;
4003
- a.download = (currentWorld ? currentWorld.id : 'custom') + '-world.json';
4004
- document.body.appendChild(a);
4005
- a.click();
4006
- document.body.removeChild(a);
4007
- URL.revokeObjectURL(url);
4008
- } catch (err) {
4009
- alert('Export failed: ' + err.message);
4010
- }
4011
- });
4012
-
4013
5041
  // ============================================
4014
- // WORLD FILE UPLOAD / PASTE
5042
+ // WORLD FILE UPLOAD / PASTE (simplified)
4015
5043
  // ============================================
4016
5044
  const uploadZone = document.getElementById('upload-zone');
4017
5045
  const uploadFileInput = document.getElementById('upload-file-input');
@@ -4020,56 +5048,43 @@ const worldJsonInput = document.getElementById('world-json-input');
4020
5048
  const loadWorldBtn = document.getElementById('load-world-btn');
4021
5049
  const uploadStatusEl = document.getElementById('upload-status');
4022
5050
 
4023
- // Drag and drop
4024
- uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
4025
- uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
4026
- uploadZone.addEventListener('drop', (e) => {
4027
- e.preventDefault();
4028
- uploadZone.classList.remove('dragover');
4029
- const file = e.dataTransfer.files[0];
4030
- if (file) readWorldFile(file);
4031
- });
4032
-
4033
- // Browse button
4034
- uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadFileInput.click(); });
4035
- uploadFileInput.addEventListener('change', () => {
4036
- if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]);
4037
- });
4038
-
4039
- // Click zone to browse
4040
- 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]); });
4041
5064
 
4042
5065
  function readWorldFile(file) {
4043
5066
  const reader = new FileReader();
4044
5067
  reader.onload = (e) => {
4045
- worldJsonInput.value = e.target.result;
4046
- uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".';
4047
- 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'; }
4048
5070
  };
4049
5071
  reader.readAsText(file);
4050
5072
  }
4051
5073
 
4052
- // Load into Runtime
4053
- loadWorldBtn.addEventListener('click', async () => {
4054
- const jsonText = worldJsonInput.value.trim();
5074
+ if (loadWorldBtn) loadWorldBtn.addEventListener('click', async () => {
5075
+ const jsonText = worldJsonInput ? worldJsonInput.value.trim() : '';
4055
5076
  if (!jsonText) {
4056
- uploadStatusEl.textContent = 'Paste or upload a world file first.';
4057
- uploadStatusEl.className = 'rule-status error';
5077
+ if (uploadStatusEl) { uploadStatusEl.textContent = 'Paste or upload a world file first.'; uploadStatusEl.className = 'rule-status error'; }
4058
5078
  return;
4059
5079
  }
4060
5080
 
4061
5081
  let worldData;
4062
- try {
4063
- worldData = JSON.parse(jsonText);
4064
- } catch (err) {
4065
- uploadStatusEl.textContent = 'Invalid JSON: ' + err.message;
4066
- 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'; }
4067
5084
  return;
4068
5085
  }
4069
5086
 
4070
- // Normalize: if the JSON is { world: {...} } or just {...}
4071
5087
  const worldPayload = worldData.world || worldData;
4072
-
4073
5088
  loadWorldBtn.textContent = 'Loading...';
4074
5089
  loadWorldBtn.disabled = true;
4075
5090
 
@@ -4082,202 +5097,50 @@ loadWorldBtn.addEventListener('click', async () => {
4082
5097
  const result = await resp.json();
4083
5098
 
4084
5099
  if (result.error) {
4085
- uploadStatusEl.textContent = result.error;
4086
- uploadStatusEl.className = 'rule-status error';
5100
+ if (uploadStatusEl) { uploadStatusEl.textContent = result.error; uploadStatusEl.className = 'rule-status error'; }
4087
5101
  } else {
4088
- uploadStatusEl.textContent = result.message;
4089
- uploadStatusEl.className = 'rule-status success';
5102
+ if (uploadStatusEl) { uploadStatusEl.textContent = result.message; uploadStatusEl.className = 'rule-status success'; }
4090
5103
 
4091
5104
  // Show loaded world info
4092
- const infoEl = document.getElementById('loaded-world-info');
4093
- infoEl.style.display = '';
4094
- document.getElementById('lw-name').textContent = result.world.title;
4095
- document.getElementById('lw-thesis').textContent = '"' + result.world.thesis + '"';
4096
- document.getElementById('lw-stats').textContent =
4097
- result.world.invariants.length + ' invariants, ' +
4098
- result.world.gates.length + ' gates, ' +
4099
- result.rulesApplied + ' rules';
4100
-
4101
- // Update active invariants in right panel
4102
- 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 =>
4103
5133
  '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
4104
- ).join('') + result.world.gates.map(g =>
4105
- '<div class="inv-item" style="color:' + (g.severity === 'critical' ? 'var(--red)' : 'var(--yellow)') + '">[' + g.id + '] ' + g.label + '</div>'
4106
5134
  ).join('');
4107
- activeInvEl.innerHTML = invHtml || '<div style="font-size:11px;color:var(--text-muted)">No invariants defined</div>';
4108
-
4109
- // Update state variables if present
4110
- if (result.world.stateVariables && result.world.stateVariables.length > 0) {
4111
- // Store as a pseudo-world so sliders render
4112
- currentWorld = {
4113
- id: 'custom-world',
4114
- title: result.world.title,
4115
- thesis: result.world.thesis,
4116
- stateVariables: result.world.stateVariables,
4117
- invariants: result.world.invariants,
4118
- gates: result.world.gates,
4119
- };
4120
- selectWorld('custom-world');
4121
- } else {
4122
- // Just set current world reference
4123
- currentWorld = {
4124
- id: 'custom-world',
4125
- title: result.world.title,
4126
- thesis: result.world.thesis,
4127
- stateVariables: [],
4128
- invariants: result.world.invariants,
4129
- gates: result.world.gates,
4130
- };
4131
- }
4132
5135
  }
4133
5136
  } catch (err) {
4134
- uploadStatusEl.textContent = 'Error: ' + err.message;
4135
- uploadStatusEl.className = 'rule-status error';
5137
+ if (uploadStatusEl) { uploadStatusEl.textContent = 'Error: ' + err.message; uploadStatusEl.className = 'rule-status error'; }
4136
5138
  }
4137
5139
 
4138
5140
  loadWorldBtn.textContent = 'Load into Runtime';
4139
5141
  loadWorldBtn.disabled = false;
4140
5142
  });
4141
5143
 
4142
- // ============================================
4143
- // PLAIN-ENGLISH RULE EDITOR
4144
- // ============================================
4145
- const ruleInput = document.getElementById('rule-input');
4146
- const parseRulesBtn = document.getElementById('parse-rules-btn');
4147
- const parsedRulesEl = document.getElementById('parsed-rules');
4148
- const ruleStatusEl = document.getElementById('rule-status');
4149
- let parsedRuleData = [];
4150
-
4151
- parseRulesBtn.addEventListener('click', async () => {
4152
- const text = ruleInput.value.trim();
4153
- if (!text) return;
4154
-
4155
- parseRulesBtn.disabled = true;
4156
- parseRulesBtn.textContent = 'Parsing...';
4157
- ruleStatusEl.textContent = '';
4158
- ruleStatusEl.className = 'rule-status';
4159
-
4160
- try {
4161
- const resp = await fetch('/api/parse-rules', {
4162
- method: 'POST',
4163
- headers: { 'Content-Type': 'application/json' },
4164
- body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'social_simulation' }),
4165
- });
4166
- const data = await resp.json();
4167
-
4168
- if (data.error) {
4169
- ruleStatusEl.textContent = data.error;
4170
- ruleStatusEl.className = 'rule-status error';
4171
- parsedRulesEl.innerHTML = '';
4172
- parsedRuleData = [];
4173
- } else {
4174
- parsedRuleData = data.rules || [];
4175
- parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
4176
- const enfType = r.enforcement || 'block';
4177
- const iconMap = { block: '&#x1F534;', allow: '&#x1F7E2;', modify: '&#x1F535;', warn: '&#x1F7E1;', pause: '&#x1F7E1;' };
4178
- const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
4179
- const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
4180
- const icon = iconMap[enfType] || '&#x1F7E2;';
4181
- const label = labelMap[enfType] || 'Rule';
4182
- const effect = effectMap[enfType] || 'Active';
4183
- return '<div class="parsed-rule enforcement-' + enfType + '">' +
4184
- '<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
4185
- '<div class="pr-desc">' + r.description + '</div>' +
4186
- '<div class="pr-patterns">' + effect + ' &bull; Matches: ' + r.intent_patterns.join(', ') + '</div>' +
4187
- '</div>';
4188
- }).join('');
4189
-
4190
- if (parsedRuleData.length > 0) {
4191
- const btnLabel = currentWorldSource === 'custom' ? 'Generate World with ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') : 'Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation';
4192
- parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">' + btnLabel + '</button>';
4193
- document.getElementById('apply-rules-btn').addEventListener('click', async () => {
4194
- try {
4195
- if (currentWorldSource === 'custom') {
4196
- // CUSTOM MODE: Generate a full governed world (state vars, gates, thesis)
4197
- const customName = document.getElementById('custom-world-name');
4198
- const worldName = (customName && customName.value) ? customName.value : 'Custom World';
4199
- const ruleText = document.getElementById('rule-input').value;
4200
-
4201
- const genResp = await fetch('/api/generate-world', {
4202
- method: 'POST',
4203
- headers: { 'Content-Type': 'application/json' },
4204
- body: JSON.stringify({ text: ruleText, name: worldName }),
4205
- });
4206
- const genData = await genResp.json();
4207
-
4208
- if (genData.status === 'generated') {
4209
- ruleStatusEl.textContent = genData.parsed.total + ' rules parsed. Full world generated: ' + genData.world.stateVariables.length + ' state variables, ' + genData.world.gates.length + ' gates, ' + genData.world.invariants.length + ' invariants.';
4210
- ruleStatusEl.className = 'rule-status success';
4211
-
4212
- // Show invariants in active panel
4213
- activeInvEl.innerHTML = genData.world.invariants.map(function(inv) {
4214
- const color = inv.enforceable ? 'var(--red)' : 'var(--green)';
4215
- return '<div class="inv-item" style="color:' + color + '">[' + inv.id + '] ' + inv.description + '</div>';
4216
- }).join('');
4217
-
4218
- // Set the generated world as the current world — with real state variables and gates
4219
- currentWorld = {
4220
- id: genData.world.id,
4221
- title: genData.world.title,
4222
- thesis: genData.world.thesis,
4223
- stateVariables: genData.world.stateVariables,
4224
- invariants: genData.world.invariants,
4225
- gates: genData.world.gates,
4226
- };
4227
- worlds.push(currentWorld);
4228
-
4229
- // Update world selector dropdown
4230
- var opt = document.createElement('option');
4231
- opt.value = currentWorld.id;
4232
- opt.textContent = currentWorld.title;
4233
- worldSelect.appendChild(opt);
4234
- worldSelect.value = currentWorld.id;
4235
-
4236
- // Render the state variable sliders
4237
- selectWorld(currentWorld.id);
4238
- document.getElementById('world-thesis').textContent = '"' + genData.world.thesis + '"';
4239
- }
4240
- } else {
4241
- // PRESET MODE: Just apply rules on top of existing world
4242
- let worldId = currentWorld ? currentWorld.id : 'social_simulation';
4243
-
4244
- const applyResp = await fetch('/api/apply-rules', {
4245
- method: 'POST',
4246
- headers: { 'Content-Type': 'application/json' },
4247
- body: JSON.stringify({ rules: parsedRuleData, worldId }),
4248
- });
4249
- const applyData = await applyResp.json();
4250
- if (applyData.status === 'applied') {
4251
- ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
4252
- ruleStatusEl.className = 'rule-status success';
4253
-
4254
- // Show rules in active invariants panel
4255
- activeInvEl.innerHTML = parsedRuleData.map(function(r) {
4256
- var enfType = r.enforcement || 'block';
4257
- var colorMap = { block: 'var(--red)', allow: 'var(--green)', modify: 'var(--blue)', warn: 'var(--yellow)', pause: 'var(--yellow)' };
4258
- var color = colorMap[enfType] || 'var(--text-secondary)';
4259
- return '<div class="inv-item" style="color:' + color + '">[' + r.id + '] ' + r.description + '</div>';
4260
- }).join('');
4261
- }
4262
- }
4263
- } catch (err) {
4264
- ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
4265
- ruleStatusEl.className = 'rule-status error';
4266
- }
4267
- });
4268
- ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
4269
- ruleStatusEl.className = 'rule-status success';
4270
- }
4271
- }
4272
- } catch (err) {
4273
- ruleStatusEl.textContent = 'Error: ' + err.message;
4274
- ruleStatusEl.className = 'rule-status error';
4275
- }
4276
-
4277
- parseRulesBtn.disabled = false;
4278
- parseRulesBtn.textContent = 'Parse Rules';
4279
- });
4280
-
4281
5144
  // ============================================
4282
5145
  // SESSION TRACKING
4283
5146
  // ============================================
@@ -4293,6 +5156,8 @@ async function pollSessionStats() {
4293
5156
  if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
4294
5157
  if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
4295
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;
4296
5161
  if (el('s-agents')) {
4297
5162
  el('s-agents').textContent = data.agents.length > 0
4298
5163
  ? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
@@ -4354,6 +5219,189 @@ async function saveExperiment() {
4354
5219
  sessionPollInterval = setInterval(pollSessionStats, 2000);
4355
5220
  pollSessionStats();
4356
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
+
4357
5405
  // ============================================
4358
5406
  // BOOT
4359
5407
  // ============================================