@neuroverseos/nv-sim 0.1.6 → 0.1.9

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.
@@ -180,12 +180,30 @@ function startInteractiveServer(port, onReady) {
180
180
  let currentSession = {
181
181
  id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
182
182
  startedAt: new Date().toISOString(),
183
- world: "trading",
183
+ world: "social_simulation",
184
184
  guardCount: 0,
185
185
  evaluations: [],
186
186
  };
187
187
  // Session snapshots for multi-run comparison
188
188
  const sessionHistory = [];
189
+ // ── Bridge Metrics Tracker ──
190
+ // Computes meaningful live metrics from external /api/evaluate calls
191
+ let bridgeEvalCount = 0;
192
+ let bridgeBlockCount = 0;
193
+ let bridgeModifyCount = 0;
194
+ function computeBridgeMetrics(decision) {
195
+ bridgeEvalCount++;
196
+ if (decision === "BLOCK")
197
+ bridgeBlockCount++;
198
+ if (decision === "MODIFY")
199
+ bridgeModifyCount++;
200
+ const totalInterventions = bridgeBlockCount + bridgeModifyCount;
201
+ // Stability = ratio of non-blocked actions (higher = more stable)
202
+ const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount) / bridgeEvalCount : 1;
203
+ // Volatility = ratio of interventions (blocks + modifies) — more interventions = more volatile
204
+ const volatility = bridgeEvalCount > 0 ? totalInterventions / bridgeEvalCount : 0;
205
+ return { stability, volatility, totalInterventions, evalCount: bridgeEvalCount };
206
+ }
189
207
  function synthesizeSessionReport() {
190
208
  // Gather all sessions to report on (history + current if it has data)
191
209
  const allSessions = [
@@ -499,7 +517,7 @@ function startInteractiveServer(port, onReady) {
499
517
  }
500
518
  if (req.url === "/api/worlds" && req.method === "GET") {
501
519
  const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
502
- const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis"];
520
+ const worldIds = ["social_simulation", "science_research"];
503
521
  const worlds = worldIds.map(id => {
504
522
  try {
505
523
  const r = resolveWorld(id);
@@ -602,7 +620,7 @@ function startInteractiveServer(port, onReady) {
602
620
  return;
603
621
  }
604
622
  // Resolve world for evaluation
605
- const worldId = payload.world ?? "trading";
623
+ const worldId = payload.world ?? "social_simulation";
606
624
  let nvWorld;
607
625
  try {
608
626
  const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
@@ -834,25 +852,35 @@ function startInteractiveServer(port, onReady) {
834
852
  const decision = verdict.status === "BLOCK" ? "BLOCK"
835
853
  : verdict.status === "PAUSE" ? "MODIFY"
836
854
  : "ALLOW";
855
+ // Compute live metrics from cumulative bridge evaluations
856
+ const bridgeMetrics = computeBridgeMetrics(decision);
837
857
  // Broadcast governance event to connected SSE clients
838
858
  broadcast({
839
859
  type: "round",
840
- round: 0,
860
+ round: bridgeMetrics.evalCount,
841
861
  totalRounds: 0,
842
862
  phase: "governed",
843
863
  reactions: [{
844
864
  stakeholder_id: payload.actor,
845
865
  reaction: payload.action,
846
- impact: 0,
866
+ impact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
847
867
  confidence: 0.5,
848
868
  trigger: "bridge",
849
869
  verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
850
870
  }],
851
- avgImpact: 0,
852
- maxVolatility: 0,
871
+ avgImpact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
872
+ maxVolatility: bridgeMetrics.volatility,
853
873
  dynamics: [],
854
874
  interventionCount: decision !== "ALLOW" ? 1 : 0,
855
875
  });
876
+ // Also broadcast a stability update so the metric refreshes live
877
+ broadcast({
878
+ type: "bridge_metrics",
879
+ stability: bridgeMetrics.stability,
880
+ volatility: bridgeMetrics.volatility,
881
+ evalCount: bridgeMetrics.evalCount,
882
+ totalInterventions: bridgeMetrics.totalInterventions,
883
+ });
856
884
  // Record in session
857
885
  currentSession.evaluations.push({
858
886
  actor: payload.actor, action: payload.action,
@@ -923,6 +951,59 @@ function startInteractiveServer(port, onReady) {
923
951
  }
924
952
  return;
925
953
  }
954
+ // Generate a full governed world from plain-English rules
955
+ // Returns a complete world: thesis, state variables, invariants, gates
956
+ if (req.url === "/api/generate-world" && req.method === "POST") {
957
+ try {
958
+ const body = await readBody(req);
959
+ const payload = JSON.parse(body);
960
+ if (!payload.text) {
961
+ jsonResponse(res, 400, { error: "text is required" });
962
+ return;
963
+ }
964
+ const { parseRulesFromText, policyToWorld } = await Promise.resolve().then(() => __importStar(require("./policyEngine")));
965
+ const parsed = parseRulesFromText(payload.text);
966
+ const worldDef = policyToWorld(parsed);
967
+ const worldName = payload.name || "Custom World";
968
+ const worldId = "custom-" + worldName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
969
+ // Apply as the active world
970
+ customGuards.length = 0;
971
+ for (let i = 0; i < parsed.rules.length; i++) {
972
+ const rule = parsed.rules[i];
973
+ customGuards.push({
974
+ id: worldDef.invariants[i]?.id ?? `custom-rule-${i}`,
975
+ label: rule.description,
976
+ description: rule.description,
977
+ category: "custom",
978
+ enforcement: (rule.category === "prohibit" || rule.category === "circuit_breaker") ? "block" : rule.category === "limit" ? "block" : "allow",
979
+ immutable: false,
980
+ intent_patterns: [...rule.keywords],
981
+ default_enabled: true,
982
+ });
983
+ }
984
+ jsonResponse(res, 200, {
985
+ status: "generated",
986
+ world: {
987
+ id: worldId,
988
+ title: worldName,
989
+ thesis: worldDef.thesis,
990
+ stateVariables: worldDef.state_variables,
991
+ invariants: worldDef.invariants,
992
+ gates: worldDef.gates ?? [],
993
+ },
994
+ parsed: {
995
+ total: parsed.summary.total,
996
+ enforced: parsed.summary.enforced,
997
+ advisory: parsed.summary.advisory,
998
+ },
999
+ rulesApplied: customGuards.length,
1000
+ });
1001
+ }
1002
+ catch (err) {
1003
+ jsonResponse(res, 400, { error: err?.message ?? "Invalid request" });
1004
+ }
1005
+ return;
1006
+ }
926
1007
  // Apply parsed rules to the active governance context
927
1008
  if (req.url === "/api/apply-rules" && req.method === "POST") {
928
1009
  try {
@@ -958,6 +1039,127 @@ function startInteractiveServer(port, onReady) {
958
1039
  }
959
1040
  return;
960
1041
  }
1042
+ // ── Clear Rules ──
1043
+ // Wipes all custom guards and resets governance to base world rules
1044
+ if (req.url === "/api/clear-rules" && req.method === "POST") {
1045
+ customGuards.length = 0;
1046
+ jsonResponse(res, 200, {
1047
+ status: "cleared",
1048
+ message: "All custom rules removed. Governance reset to base world rules.",
1049
+ });
1050
+ return;
1051
+ }
1052
+ // ── Load World File ──
1053
+ // Accept a full world definition JSON and use it as the active world
1054
+ if (req.url === "/api/load-world-file" && req.method === "POST") {
1055
+ try {
1056
+ const body = await readBody(req);
1057
+ const payload = JSON.parse(body);
1058
+ if (!payload.world) {
1059
+ jsonResponse(res, 400, { error: "world object is required" });
1060
+ return;
1061
+ }
1062
+ const w = payload.world;
1063
+ // Parse any plain-English rules in the world file into guards
1064
+ customGuards.length = 0;
1065
+ if (w.rules && Array.isArray(w.rules)) {
1066
+ for (let i = 0; i < w.rules.length; i++) {
1067
+ const rule = w.rules[i];
1068
+ if (rule.intent_patterns && rule.intent_patterns.length > 0) {
1069
+ // Already structured rule
1070
+ customGuards.push({
1071
+ id: rule.id || `world-rule-${i}`,
1072
+ label: rule.description,
1073
+ description: rule.description,
1074
+ category: "world-file",
1075
+ enforcement: rule.enforcement || "block",
1076
+ immutable: false,
1077
+ intent_patterns: rule.intent_patterns,
1078
+ default_enabled: true,
1079
+ });
1080
+ }
1081
+ else {
1082
+ // Plain-English rule — parse it
1083
+ const parsed = parseNaturalLanguageRule(rule.description, i);
1084
+ if (parsed) {
1085
+ customGuards.push({
1086
+ id: parsed.id,
1087
+ label: parsed.description,
1088
+ description: parsed.description,
1089
+ category: "world-file",
1090
+ enforcement: parsed.enforcement,
1091
+ immutable: false,
1092
+ intent_patterns: parsed.intent_patterns,
1093
+ default_enabled: true,
1094
+ });
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+ // Build the response world definition for the UI
1100
+ const loadedWorld = {
1101
+ id: "custom-world",
1102
+ title: w.name || "Custom World",
1103
+ thesis: w.thesis || "User-defined world",
1104
+ stateVariables: w.state_variables || [],
1105
+ invariants: (w.invariants || []).map(inv => ({
1106
+ id: inv.id,
1107
+ description: inv.description,
1108
+ enforceable: inv.enforceable !== false,
1109
+ })),
1110
+ gates: (w.gates || []).map(g => ({
1111
+ id: g.id,
1112
+ label: g.label,
1113
+ condition: g.condition,
1114
+ severity: g.severity || "warning",
1115
+ })),
1116
+ rulesApplied: customGuards.length,
1117
+ };
1118
+ jsonResponse(res, 200, {
1119
+ status: "loaded",
1120
+ world: loadedWorld,
1121
+ rulesApplied: customGuards.length,
1122
+ message: `World "${loadedWorld.title}" loaded with ${loadedWorld.invariants.length} invariants, ${loadedWorld.gates.length} gates, and ${customGuards.length} rules.`,
1123
+ });
1124
+ }
1125
+ catch (err) {
1126
+ jsonResponse(res, 400, { error: "Invalid world file JSON" });
1127
+ }
1128
+ return;
1129
+ }
1130
+ // ── Export World File ──
1131
+ // Exports the current world configuration (base world + custom rules + overrides) as a world file
1132
+ if (req.url === "/api/export-world" && req.method === "GET") {
1133
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
1134
+ const worldId = currentSession.world || "social_simulation";
1135
+ let baseWorld;
1136
+ try {
1137
+ baseWorld = resolveWorld(worldId);
1138
+ }
1139
+ catch {
1140
+ baseWorld = null;
1141
+ }
1142
+ const exportedWorld = {
1143
+ name: baseWorld?.title || worldId,
1144
+ thesis: baseWorld?.world?.thesis || "Exported world",
1145
+ state_variables: baseWorld?.world?.state_variables || [],
1146
+ invariants: baseWorld?.world?.invariants || [],
1147
+ gates: baseWorld?.world?.gates || [],
1148
+ rules: customGuards.map(g => ({
1149
+ id: g.id,
1150
+ description: g.description,
1151
+ enforcement: g.enforcement,
1152
+ intent_patterns: g.intent_patterns,
1153
+ })),
1154
+ };
1155
+ res.writeHead(200, {
1156
+ "Content-Type": "application/json",
1157
+ "Content-Disposition": `attachment; filename="${worldId}-world.json"`,
1158
+ "Access-Control-Allow-Origin": "*",
1159
+ });
1160
+ res.end(JSON.stringify({ world: exportedWorld }, null, 2));
1161
+ return;
1162
+ }
961
1163
  // ── Session Reporting Endpoints ──
962
1164
  // Connect the serve runtime to the enforce reporting pipeline.
963
1165
  // Users can request reports, stats, and recommendations from live governance data.
@@ -1122,6 +1324,10 @@ function startInteractiveServer(port, onReady) {
1122
1324
  guardCount: customGuards.length,
1123
1325
  evaluations: [],
1124
1326
  };
1327
+ // Reset bridge metrics counters
1328
+ bridgeEvalCount = 0;
1329
+ bridgeBlockCount = 0;
1330
+ bridgeModifyCount = 0;
1125
1331
  jsonResponse(res, 200, {
1126
1332
  status: "reset",
1127
1333
  newSessionId: currentSession.id,
@@ -1200,7 +1406,7 @@ function startInteractiveServer(port, onReady) {
1200
1406
  jsonResponse(res, 200, { status: "started", adapter: payload.adapterId });
1201
1407
  // Resolve world for governance evaluation
1202
1408
  const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
1203
- const resolved = resolveWorld(payload.worldId ?? "trading");
1409
+ const resolved = resolveWorld(payload.worldId ?? "social_simulation");
1204
1410
  const world = { ...resolved.world };
1205
1411
  if (payload.stateOverrides) {
1206
1412
  const updatedVars = world.state_variables.map(sv => {
@@ -1515,42 +1721,80 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1515
1721
  .variant-card .vbase { display: inline-block; font-size: 9px; padding: 1px 5px; background: var(--accent-bg); color: var(--accent); border-radius: 3px; margin-top: 3px; }
1516
1722
 
1517
1723
  /* RIGHT PANEL — Simulation viewer */
1518
- .viewer { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
1519
- .viewer-top { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); }
1520
- .viewer-mid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); overflow: hidden; }
1521
- .viewer-bottom { background: var(--bg-primary); border-top: 1px solid var(--border); padding: 12px 16px; max-height: 180px; overflow-y: auto; }
1522
-
1523
- .vpanel { background: var(--bg-primary); padding: 14px; overflow-y: auto; }
1524
- .vpanel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
1525
-
1526
- /* Metrics */
1527
- .metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
1528
- .metric-box { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 10px; text-align: center; }
1529
- .metric-box .value { font-size: 20px; font-weight: 700; color: var(--text-primary); }
1530
- .metric-box .label { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
1531
- .metric-box.good .value { color: var(--green); }
1532
- .metric-box.bad .value { color: var(--red); }
1533
- .metric-box.warn .value { color: var(--yellow); }
1534
-
1535
- /* Agent bars */
1536
- .agent-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; }
1537
- .agent-name { width: 130px; color: var(--text-secondary); flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1538
- .impact-bar-bg { flex: 1; height: 14px; background: var(--bg-surface); border-radius: 3px; position: relative; overflow: hidden; }
1539
- .impact-bar { height: 100%; border-radius: 3px; transition: width 0.4s ease; position: absolute; top: 0; }
1540
- .impact-bar.positive { background: var(--green); right: 50%; }
1541
- .impact-bar.negative { background: var(--red); left: 50%; }
1542
- .impact-val { width: 44px; text-align: right; color: var(--text-secondary); font-size: 10px; }
1724
+ .viewer { display: grid; grid-template-rows: auto auto auto auto auto; overflow-y: auto; height: 100%; gap: 0; }
1725
+
1726
+ /* Outcome panel */
1727
+ .outcome-panel { padding: 20px 24px 16px; border-bottom: 1px solid var(--border); }
1728
+ .outcome-statement { font-size: 16px; font-weight: 600; color: var(--text-primary); line-height: 1.4; margin-bottom: 12px; }
1729
+ .outcome-empty { font-size: 13px; color: var(--text-faint); font-style: italic; }
1730
+ .confidence-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 8px; }
1731
+ .confidence-card { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 8px 10px; }
1732
+ .confidence-card .cc-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
1733
+ .confidence-card .cc-value { font-size: 13px; font-weight: 600; }
1734
+ .confidence-card .cc-value.good { color: var(--green); }
1735
+ .confidence-card .cc-value.warn { color: var(--yellow); }
1736
+ .confidence-card .cc-value.bad { color: var(--red); }
1737
+ .outcome-context { font-size: 10px; color: var(--text-faint); margin-top: 6px; }
1738
+
1739
+ /* Behavior panel */
1740
+ .behavior-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
1741
+ .behavior-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
1742
+ .behavior-shifts { margin-bottom: 10px; }
1743
+ .behavior-shift-item { font-size: 12px; color: var(--text-secondary); padding: 4px 0 4px 12px; border-left: 2px solid var(--accent); line-height: 1.5; margin-bottom: 4px; }
1744
+ .behavior-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
1745
+
1746
+ .activity-toggle { font-size: 10px; color: var(--text-muted); cursor: pointer; padding: 6px 0; margin-top: 4px; }
1747
+ .activity-toggle:hover { color: var(--text-secondary); }
1748
+ .activity-timeline { display: none; max-height: 180px; overflow-y: auto; margin-top: 6px; }
1749
+ .activity-timeline.open { display: block; }
1750
+ .activity-item { font-size: 11px; color: var(--text-secondary); padding: 3px 0; border-bottom: 1px solid var(--border); }
1751
+ .activity-item:last-child { border-bottom: none; }
1752
+ .activity-agent { color: var(--blue); font-weight: 500; }
1753
+
1754
+ /* Why panel */
1755
+ .why-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
1756
+ .why-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
1757
+ .why-item { font-size: 12px; color: var(--text-secondary); padding: 4px 0 4px 12px; border-left: 2px solid var(--green); line-height: 1.5; margin-bottom: 4px; }
1758
+ .why-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
1759
+
1760
+ /* Export bar */
1761
+ .export-bar { display: flex; gap: 8px; padding: 12px 24px; border-bottom: 1px solid var(--border); background: var(--bg-secondary); }
1762
+ .export-btn { padding: 6px 14px; font-size: 11px; font-weight: 600; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-surface); color: var(--text-secondary); cursor: pointer; font-family: inherit; transition: all 0.2s; }
1763
+ .export-btn:hover { background: var(--bg-elevated); color: var(--text-primary); }
1764
+ .audit-btn { margin-left: auto; color: var(--text-faint); }
1765
+
1766
+ /* Audit overlay */
1767
+ .audit-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; background: rgba(0,0,0,0.7); }
1768
+ .audit-overlay.open { display: flex; align-items: center; justify-content: center; }
1769
+ .audit-modal { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 8px; width: 90%; max-width: 900px; max-height: 85vh; overflow-y: auto; padding: 0; }
1770
+ .audit-header { display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg-primary); z-index: 1; }
1771
+ .audit-header h2 { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; }
1772
+ .audit-close { margin-left: auto; padding: 4px 12px; font-size: 12px; background: none; border: 1px solid var(--border); border-radius: 4px; color: var(--text-muted); cursor: pointer; font-family: inherit; }
1773
+ .audit-close:hover { color: var(--text-primary); }
1774
+ .audit-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
1775
+ .audit-section:last-child { border-bottom: none; }
1776
+ .audit-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
1777
+ .audit-section .rule-card { margin-bottom: 6px; }
1778
+
1779
+ /* Print styles for PDF export */
1780
+ @media print {
1781
+ body { background: #fff !important; color: #111 !important; }
1782
+ .controls, .export-bar, .audit-overlay, .activity-toggle { display: none !important; }
1783
+ .viewer { overflow: visible !important; height: auto !important; }
1784
+ .outcome-panel, .behavior-panel, .why-panel { border: 1px solid #ddd !important; margin-bottom: 8px; border-radius: 6px; }
1785
+ .confidence-card { border: 1px solid #ddd !important; }
1786
+ * { color: #111 !important; background: #fff !important; border-color: #ddd !important; }
1787
+ .cc-value.good { color: #16a34a !important; }
1788
+ .cc-value.warn { color: #ca8a04 !important; }
1789
+ .cc-value.bad { color: #dc2626 !important; }
1790
+ }
1543
1791
  .center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
1544
1792
  .verdict { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-left: 4px; }
1545
1793
  .verdict.ALLOW { background: var(--green-bg); color: var(--green); }
1546
1794
  .verdict.BLOCK { background: var(--red-bg); color: var(--red); }
1547
1795
  .verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
1548
1796
 
1549
- /* Chart */
1550
- .chart-container { position: relative; height: 100%; min-height: 150px; }
1551
- canvas { width: 100% !important; height: 100% !important; }
1552
-
1553
- /* Simulation Trace */
1797
+ /* Simulation Trace (audit only) */
1554
1798
  .trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
1555
1799
  .trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
1556
1800
  .trace-round-header:hover { background: var(--bg-elevated); }
@@ -1660,6 +1904,60 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1660
1904
  .integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
1661
1905
  .integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
1662
1906
 
1907
+ /* Language tabs for integration code */
1908
+ .lang-tabs { display: flex; gap: 2px; margin-bottom: 6px; }
1909
+ .lang-tab { padding: 4px 10px; font-size: 10px; font-weight: 600; color: var(--text-muted); background: transparent; border: 1px solid transparent; border-bottom: none; border-radius: 4px 4px 0 0; cursor: pointer; font-family: inherit; transition: all 0.2s; }
1910
+ .lang-tab:hover { color: var(--text-secondary); }
1911
+ .lang-tab.active { color: var(--accent); background: var(--bg-surface); border-color: var(--bg-elevated); }
1912
+ .lang-code-panel { display: none; }
1913
+ .lang-code-panel.active { display: block; }
1914
+ .integrate-hint { font-size: 10px; color: var(--text-muted); margin-top: 6px; line-height: 1.5; }
1915
+ .integrate-hint strong { color: var(--text-secondary); }
1916
+ .integrate-works { font-size: 10px; color: var(--text-faint); margin-top: 8px; line-height: 1.6; }
1917
+ .integrate-works strong { color: var(--text-muted); }
1918
+
1919
+ /* Framework selector tabs */
1920
+ .fw-tabs { display: flex; gap: 4px; margin-bottom: 10px; }
1921
+ .fw-tab { flex: 1; padding: 8px 6px; font-size: 10px; font-weight: 700; color: var(--text-muted); background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-family: inherit; transition: all 0.2s; text-align: center; line-height: 1.3; }
1922
+ .fw-tab:hover { border-color: var(--text-muted); color: var(--text-secondary); }
1923
+ .fw-tab.active { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
1924
+ .fw-tab .fw-sub { display: block; font-size: 8px; font-weight: 400; color: var(--text-faint); margin-top: 2px; }
1925
+ .fw-tab.active .fw-sub { color: var(--accent); opacity: 0.7; }
1926
+ .fw-panel { display: none; }
1927
+ .fw-panel.active { display: block; }
1928
+
1929
+ /* Architecture callout */
1930
+ .arch-callout { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; }
1931
+ .arch-callout .arch-title { font-size: 10px; font-weight: 700; color: var(--text-secondary); margin-bottom: 4px; }
1932
+ .arch-flow { font-size: 10px; color: var(--text-muted); font-family: monospace; line-height: 1.6; }
1933
+ .arch-flow .arch-yes { color: var(--green); }
1934
+ .arch-flow .arch-no { color: var(--red); }
1935
+ .arch-warn { font-size: 9px; color: var(--red); margin-top: 6px; padding: 4px 8px; background: #2d0606; border-radius: 4px; line-height: 1.5; }
1936
+
1937
+ /* Responsive / Adaptive Panels */
1938
+ @media (max-width: 1200px) {
1939
+ .layout { grid-template-columns: 300px 1fr; }
1940
+ .controls { padding: 12px; }
1941
+ }
1942
+ @media (max-width: 960px) {
1943
+ .layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; height: auto; min-height: 100vh; }
1944
+ .controls { border-right: none; border-bottom: 1px solid var(--border); max-height: 50vh; overflow-y: auto; }
1945
+ .viewer { min-height: 60vh; }
1946
+ .viewer-top { grid-template-columns: 1fr; }
1947
+ .viewer-mid { grid-template-columns: 1fr; }
1948
+ .metric-grid { grid-template-columns: repeat(2, 1fr); }
1949
+ }
1950
+ @media (max-width: 640px) {
1951
+ .header h1 { font-size: 14px; }
1952
+ .header .sub { font-size: 10px; }
1953
+ .controls { padding: 10px; }
1954
+ .ctrl-section h3 { font-size: 10px; }
1955
+ .confidence-grid { grid-template-columns: 1fr; }
1956
+ .agent-name { width: 80px; }
1957
+ .integrate-code { font-size: 9px; padding: 6px; }
1958
+ .scenario-btn { padding: 6px; }
1959
+ }
1960
+
1663
1961
  /* Rule editor */
1664
1962
  .rule-editor { margin-top: 8px; }
1665
1963
  .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; }
@@ -1687,13 +1985,70 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1687
1985
  .rule-status.error { color: var(--red); }
1688
1986
  .rule-examples { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.6; }
1689
1987
  .rule-examples code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
1988
+
1989
+ /* World Action Bar */
1990
+ .world-action-bar { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
1991
+ .btn-world-action { flex: 1; min-width: 0; padding: 6px 4px; font-size: 10px; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font-family: inherit; text-align: center; transition: all 0.2s; white-space: nowrap; }
1992
+ .btn-world-action:hover { border-color: var(--accent); color: var(--accent); }
1993
+ .btn-world-action.btn-export { color: var(--green); border-color: var(--green); opacity: 0.7; }
1994
+ .btn-world-action.btn-export:hover { opacity: 1; }
1995
+
1996
+ /* World Source Tabs */
1997
+ .world-source-tabs { display: flex; gap: 4px; }
1998
+ .ws-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 10px 6px; background: var(--bg-surface); border: 2px solid var(--border); border-radius: 6px; cursor: pointer; transition: all 0.2s; text-align: center; }
1999
+ .ws-tab:hover { border-color: var(--text-muted); }
2000
+ .ws-tab.active { border-color: var(--accent); background: var(--accent-bg); }
2001
+ .ws-tab input[type="radio"] { display: none; }
2002
+ .ws-label { font-size: 11px; font-weight: 700; color: var(--text-primary); }
2003
+ .ws-hint { font-size: 9px; color: var(--text-muted); margin-top: 2px; }
2004
+ .ws-tab.active .ws-label { color: var(--accent); }
2005
+
2006
+ /* World Source Panels */
2007
+ .world-source-panel { animation: fadeIn 0.2s ease; }
2008
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
2009
+
2010
+ /* Custom World Header */
2011
+ .custom-world-header { margin-bottom: 12px; }
2012
+ .world-name-input { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
2013
+ .world-name-input:focus { border-color: var(--accent); outline: none; }
2014
+ .world-thesis-input { width: 100%; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 11px; resize: none; height: 36px; }
2015
+ .world-thesis-input:focus { border-color: var(--accent); outline: none; }
2016
+
2017
+ /* Rule editor enhancements */
2018
+ .rule-editor-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600; }
2019
+ .rule-input-large { min-height: 120px; }
2020
+ .btn-generate-world { background: var(--accent); color: #fff; margin-top: 8px; padding: 10px; font-size: 12px; font-weight: 700; width: 100%; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
2021
+ .btn-generate-world:hover { filter: brightness(1.1); }
2022
+
2023
+ /* Upload Zone */
2024
+ .upload-zone { border: 2px dashed var(--border-subtle); border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.2s; margin-bottom: 12px; }
2025
+ .upload-zone:hover, .upload-zone.dragover { border-color: var(--accent); background: var(--accent-bg); }
2026
+ .upload-icon { font-size: 28px; margin-bottom: 8px; }
2027
+ .upload-label { font-size: 12px; color: var(--text-secondary); }
2028
+ .upload-or { font-size: 10px; color: var(--text-faint); margin: 8px 0; }
2029
+ .btn-upload-browse { background: var(--bg-elevated); color: var(--text-primary); border: 1px solid var(--border); padding: 6px 16px; border-radius: 4px; cursor: pointer; font-family: inherit; font-size: 11px; }
2030
+ .upload-paste-section { margin-bottom: 12px; }
2031
+ .btn-load-world { background: var(--green); color: #0a0a0a; width: 100%; padding: 10px; font-size: 12px; font-weight: 700; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
2032
+ .btn-load-world:hover { filter: brightness(0.9); }
2033
+
2034
+ /* Loaded World Card */
2035
+ .loaded-world-card { background: var(--green-bg); border: 1px solid var(--green); border-radius: 6px; padding: 12px; margin-top: 10px; }
2036
+ .lw-name { font-size: 13px; font-weight: 700; color: var(--green); }
2037
+ .lw-thesis { font-size: 11px; color: var(--text-secondary); margin-top: 4px; font-style: italic; }
2038
+ .lw-stats { font-size: 10px; color: var(--text-muted); margin-top: 6px; }
2039
+
2040
+ /* Schema Reference */
2041
+ .schema-ref { font-size: 10px; color: var(--text-muted); }
2042
+ .schema-item { padding: 3px 0; border-bottom: 1px solid var(--border); }
2043
+ .schema-item:last-child { border-bottom: none; }
2044
+ .schema-item code { color: var(--accent); background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; }
1690
2045
  </style>
1691
2046
  </head>
1692
2047
  <body>
1693
2048
  <div class="header">
1694
2049
  <div style="display:flex;align-items:center">
1695
2050
  <h1>NV-SIM</h1>
1696
- <span class="sub">Scenario Control Platform</span>
2051
+ <span class="sub">Governance Runtime</span>
1697
2052
  </div>
1698
2053
  <div class="header-right">
1699
2054
  <button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">Light Mode</button>
@@ -1704,55 +2059,148 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1704
2059
  <div class="layout">
1705
2060
  <!-- LEFT: CONTROLS -->
1706
2061
  <div class="controls" id="controls-panel">
1707
- <!-- Simulation Engine selector -->
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
+
2070
+ <!-- World Source selector -->
1708
2071
  <div class="ctrl-section">
1709
- <h3>Simulation Engine</h3>
1710
- <div class="ctrl-row">
1711
- <select id="engine-select">
1712
- <option value="nv-sim" selected>NV-SIM (Built-in)</option>
1713
- </select>
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>
1714
2089
  </div>
1715
- <div id="engine-status" style="font-size:10px;color:#555;margin-top:4px"></div>
1716
2090
  </div>
1717
2091
 
1718
- <!-- World selector -->
1719
- <div class="ctrl-section">
1720
- <h3>World</h3>
1721
- <div class="ctrl-row">
1722
- <select id="world-select"></select>
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>
2100
+ </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>
2106
+ </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>
1723
2112
  </div>
1724
- <div id="world-thesis" class="world-thesis"></div>
1725
2113
  </div>
1726
2114
 
1727
- <!-- State variables (dynamic sliders) -->
1728
- <div class="ctrl-section" id="state-vars-section" style="display:none">
1729
- <h3>World Rules</h3>
1730
- <div id="state-vars"></div>
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>
2139
+ </div>
2140
+
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>
1731
2151
  </div>
1732
2152
 
1733
- <!-- Plain-English Rule Editor -->
1734
- <div class="ctrl-section">
1735
- <h3>Write Rules in Plain English</h3>
1736
- <div class="rule-editor">
1737
- <textarea class="rule-input" id="rule-input" placeholder="Type rules in plain English, one per line:&#10;&#10;Block panic selling during high volatility&#10;Limit leverage to 5x maximum&#10;Pause any trade over $10M for review"></textarea>
1738
- <button class="btn btn-parse" id="parse-rules-btn">Parse Rules</button>
1739
- <div id="parsed-rules" class="parsed-rules"></div>
1740
- <div id="rule-status" class="rule-status"></div>
1741
- <div class="rule-examples">
1742
- Examples:<br>
1743
- <code>Block panic selling</code><br>
1744
- <code>Limit leverage to 3x</code><br>
1745
- <code>Pause large trades for review</code><br>
1746
- <code>Allow hedging positions</code><br>
1747
- <code>Block short selling during circuit breaker</code>
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>
2157
+ <div class="upload-zone" id="upload-zone">
2158
+ <div class="upload-icon">&#x1F4C4;</div>
2159
+ <div class="upload-label">Drop a .json or .nv-world file here</div>
2160
+ <div class="upload-or">or</div>
2161
+ <button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
2162
+ <input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
2163
+ </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>
2167
+ </div>
2168
+ <button class="btn btn-load-world" id="load-world-btn">Load into Runtime</button>
2169
+ <div id="upload-status" class="rule-status"></div>
2170
+
2171
+ <!-- Loaded world info -->
2172
+ <div id="loaded-world-info" style="display:none">
2173
+ <div class="loaded-world-card">
2174
+ <div class="lw-name" id="lw-name"></div>
2175
+ <div class="lw-thesis" id="lw-thesis"></div>
2176
+ <div class="lw-stats" id="lw-stats"></div>
2177
+ </div>
2178
+ </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>
1748
2191
  </div>
1749
2192
  </div>
1750
2193
  </div>
1751
2194
 
1752
- <!-- Scenario presets -->
1753
- <div class="ctrl-section">
1754
- <h3>Scenario Presets</h3>
1755
- <div id="scenario-list"></div>
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>
2202
+ </div>
2203
+ <div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
1756
2204
  </div>
1757
2205
 
1758
2206
  <!-- Narrative injection -->
@@ -1796,37 +2244,207 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1796
2244
 
1797
2245
  <!-- Saved variants -->
1798
2246
  <div class="ctrl-section" style="margin-top:16px">
1799
- <h3>Saved Variants</h3>
2247
+ <h3>Your Worlds</h3>
1800
2248
  <div id="variant-list"><div style="font-size:11px;color:#333">No saved variants yet</div></div>
1801
2249
  </div>
1802
2250
 
1803
2251
  <!-- Integration Quick-Start -->
1804
2252
  <div class="ctrl-section" style="margin-top:16px">
1805
- <h3>Integrate Your Simulator</h3>
1806
- <div class="integrate-section">
1807
- <h4>Connect in 3 lines</h4>
1808
- <div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate
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>
1809
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>
1810
2344
  verdict = evaluate(
1811
- actor=<span class="str">"agent_1"</span>,
1812
- action=<span class="str">"panic_sell"</span>,
1813
- world=<span class="str">"trading"</span>
2345
+ actor=<span class="str">"Harry"</span>,
2346
+ action=action,
2347
+ world=<span class="str">"research"</span>
1814
2348
  )
1815
2349
 
1816
- <span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
1817
- action = <span class="str">"hold"</span> <span class="comment"># adapted</span></div>
1818
- <div class="integrate-endpoint">
1819
- Endpoint: <code id="integrate-url">POST /api/evaluate</code>
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>
1820
2356
  </div>
1821
- <div style="font-size:10px;color:#444;margin-top:6px">
1822
- Fail-open · 500ms timeout · Stateless
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>
1823
2402
  </div>
1824
- <div style="margin-top:8px;font-size:10px">
1825
- <span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> replaced
1826
- <span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> constrained
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
1827
2441
  <span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
1828
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>
1829
2446
  </div>
2447
+ </div>
1830
2448
 
1831
2449
  <!-- Session Report Panel -->
1832
2450
  <div class="ctrl-section" id="session-panel">
@@ -1850,86 +2468,308 @@ verdict = evaluate(
1850
2468
 
1851
2469
  <!-- RIGHT: VIEWER -->
1852
2470
  <div class="viewer">
1853
- <div class="viewer-top">
1854
- <div class="vpanel">
1855
- <h2>Live Metrics</h2>
1856
- <div class="metric-grid">
1857
- <div class="metric-box"><div class="value" id="m-stability">--</div><div class="label">Stability</div></div>
1858
- <div class="metric-box"><div class="value" id="m-volatility">--</div><div class="label">Volatility</div></div>
1859
- <div class="metric-box"><div class="value" id="m-round">--</div><div class="label">Round</div></div>
1860
- <div class="metric-box"><div class="value" id="m-interventions">0</div><div class="label">Interventions</div></div>
1861
- </div>
2471
+
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>
1862
2476
  </div>
1863
- <div class="vpanel">
1864
- <h2>World Rules Active</h2>
1865
- <div id="active-invariants"></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>
2489
+ </div>
1866
2490
  </div>
2491
+ <div class="outcome-context" id="outcome-context"></div>
1867
2492
  </div>
1868
2493
 
1869
- <div class="viewer-mid">
1870
- <div class="vpanel" id="agents-panel">
1871
- <h2>Agent Impacts</h2>
1872
- <div id="agents">
1873
- <div class="empty-state"><div class="icon">&gt;_</div><div class="msg">Configure world and run simulation</div><div class="hint">Adjust rules on the left, then press Run</div></div>
1874
- </div>
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>
1875
2499
  </div>
1876
- <div class="vpanel">
1877
- <h2>Impact Timeline</h2>
1878
- <div class="chart-container"><canvas id="chart"></canvas></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
1879
2502
  </div>
2503
+ <div class="activity-timeline" id="activity-timeline"></div>
1880
2504
  </div>
1881
2505
 
1882
- <!-- System Shift Card — the demo moment -->
1883
- <div id="system-shift" class="system-shift">
1884
- <div class="ss-header">
1885
- <div class="ss-icon"></div>
1886
- <span class="ss-title">System Shift</span>
1887
- </div>
1888
- <div class="ss-rule" id="ss-rule"></div>
1889
- <div class="ss-scale" id="ss-scale"></div>
1890
- <div class="ss-flow">
1891
- <span>Rule</span><span class="ss-flow-arrow">→</span>
1892
- <span>Behavioral Shift</span><span class="ss-flow-arrow">→</span>
1893
- <span>Emergent Pattern</span><span class="ss-flow-arrow">→</span>
1894
- <span>System Outcome</span>
1895
- </div>
1896
- <div class="ss-body">
1897
- <div class="ss-section">
1898
- <div class="ss-section-label">Behavioral Shift</div>
1899
- <div class="ss-adapt-rate" id="ss-adapt-rate"></div>
1900
- <div class="ss-adapt-desc" id="ss-adapt-desc"></div>
1901
- <div id="ss-shifts"></div>
1902
- </div>
1903
- <div class="ss-section">
1904
- <div class="ss-section-label">What Emerged</div>
1905
- <div id="ss-patterns"></div>
1906
- </div>
1907
- <div class="ss-section">
1908
- <div class="ss-section-label">System Outcome</div>
1909
- <div id="ss-impacts"></div>
1910
- </div>
1911
- <div class="ss-section">
1912
- <div class="ss-section-label">What Actually Happened</div>
1913
- <div class="ss-narrative" id="ss-narrative"></div>
1914
- </div>
1915
- </div>
1916
- <button class="ss-raw-toggle" id="ss-raw-toggle">
1917
- <span class="arrow">▶</span> View raw detail
1918
- </button>
1919
- <div class="ss-raw-detail" id="ss-raw-detail">
1920
- <div class="ss-raw-list" id="ss-raw-list"></div>
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>
1921
2511
  </div>
1922
2512
  </div>
1923
2513
 
1924
- <div class="viewer-bottom">
1925
- <h2 style="font-size:11px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Simulation Trace <span id="trace-source" style="color:#4ade80;font-weight:600;margin-left:6px"></span> <span style="color:#333;font-weight:400">— Events → Agents → Rules → Outcomes</span></h2>
2514
+ <!-- LAYER 4: EXPORT -->
2515
+ <div class="export-bar">
2516
+ <button class="export-btn" onclick="exportPDF()">Download PDF</button>
2517
+ <button class="export-btn" onclick="exportCSV()">Export CSV</button>
2518
+ <button class="export-btn" onclick="copyShareSummary()">Copy Summary</button>
2519
+ <button class="export-btn audit-btn" onclick="openAudit()">Audit Trail</button>
2520
+ </div>
2521
+
2522
+ <!-- Hidden elements for data tracking (not displayed) -->
2523
+ <div style="display:none">
2524
+ <span id="m-stability">--</span>
2525
+ <span id="m-volatility">--</span>
2526
+ <span id="m-round">--</span>
2527
+ <span id="m-interventions">0</span>
2528
+ <span id="trace-source"></span>
2529
+ <div id="agents"></div>
2530
+ <div id="active-invariants"></div>
1926
2531
  <div id="log"></div>
1927
2532
  </div>
1928
2533
  </div>
2534
+
2535
+ <!-- LAYER 5: AUDIT OVERLAY (separate from dashboard) -->
2536
+ <div class="audit-overlay" id="audit-overlay" onclick="if(event.target===this)closeAudit()">
2537
+ <div class="audit-modal">
2538
+ <div class="audit-header">
2539
+ <h2>Audit Trail</h2>
2540
+ <button class="audit-close" onclick="closeAudit()">Close</button>
2541
+ </div>
2542
+ <div class="audit-section" id="audit-rules">
2543
+ <h3>Active Rules</h3>
2544
+ <div id="audit-rules-content"></div>
2545
+ </div>
2546
+ <div class="audit-section" id="audit-verdicts">
2547
+ <h3>Verdict Log</h3>
2548
+ <div id="audit-verdicts-content"></div>
2549
+ </div>
2550
+ <div class="audit-section" id="audit-trace">
2551
+ <h3>Detailed Trace</h3>
2552
+ <div id="audit-trace-content"></div>
2553
+ </div>
2554
+ </div>
2555
+ </div>
1929
2556
  </div>
1930
2557
 
1931
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"><\/script>
1932
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
+ }
2570
+
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
+ }
2578
+
2579
+ // ============================================
2580
+ // OUTCOME INTELLIGENCE LAYER
2581
+ // ============================================
2582
+
2583
+ function generateOutcome(stability, volatility, shiftData, worldThesis) {
2584
+ var st = stability || 0;
2585
+ var vol = volatility || 0;
2586
+ var blockRatio = shiftData.total > 0 ? shiftData.blocks / shiftData.total : 0;
2587
+ var thesis = worldThesis || '';
2588
+
2589
+ // Determine outcome direction + dominant behavior (state + what agents did)
2590
+ var direction = '';
2591
+ var behavior = '';
2592
+ if (st > 0.7 && vol < 0.3) {
2593
+ direction = 'stabilized';
2594
+ behavior = 'agents shifted toward safer positions';
2595
+ } else if (st > 0.7 && vol >= 0.3) {
2596
+ direction = 'held under pressure';
2597
+ behavior = 'agents maintained cautious strategies despite volatility';
2598
+ } else if (st > 0.4 && vol < 0.5) {
2599
+ direction = 'partially converged';
2600
+ behavior = 'agents split between aggressive and conservative approaches';
2601
+ } else if (st <= 0.4 && vol >= 0.5) {
2602
+ direction = 'fragmented';
2603
+ behavior = 'agents competed with conflicting strategies';
2604
+ } else if (st <= 0.4) {
2605
+ direction = 'remains uncertain';
2606
+ behavior = 'agents failed to find a dominant strategy';
2607
+ } else {
2608
+ direction = 'showed mixed results';
2609
+ behavior = 'agents oscillated between risk-taking and caution';
2610
+ }
2611
+
2612
+ // Enrich with world context if available
2613
+ if (thesis) {
2614
+ var nouns = thesis.match(/\b(market|research|supply|trade|financial|climate|regulatory|investment|innovation|security)\b/i);
2615
+ if (nouns) {
2616
+ var subject = nouns[1].charAt(0).toUpperCase() + nouns[1].slice(1).toLowerCase();
2617
+ return subject + ' ' + direction + ' as ' + behavior;
2618
+ }
2619
+ }
2620
+
2621
+ // Build behavior-enriched statement — always state + dominant behavior
2622
+ if (blockRatio > 0.4 && st > 0.6) return 'System ' + direction + ' after agents abandoned high-risk strategies';
2623
+ if (blockRatio > 0.2) return 'Outcome ' + direction + ' as ' + behavior;
2624
+ if (shiftData.patterns && shiftData.patterns.length > 0) return 'Outcome ' + direction + ' — ' + behavior;
2625
+ return 'Outcome ' + direction + ' as ' + behavior;
2626
+ }
2627
+
2628
+ function computeConfidence(stability, volatility, interventions, total) {
2629
+ var interventionRate = total > 0 ? interventions / total : 0;
2630
+ var score = stability * 0.5 + (1 - volatility) * 0.3 + (1 - interventionRate) * 0.2;
2631
+
2632
+ var strength, evidence, risk, strengthCls, evidenceCls, riskCls;
2633
+
2634
+ if (score > 0.75) {
2635
+ strength = 'Strong'; strengthCls = 'good';
2636
+ evidence = 'Solid'; evidenceCls = 'good';
2637
+ risk = 'Low'; riskCls = 'good';
2638
+ } else if (score > 0.55) {
2639
+ strength = 'Moderate'; strengthCls = 'warn';
2640
+ evidence = stability > 0.6 ? 'Solid' : 'Mixed'; evidenceCls = stability > 0.6 ? 'good' : 'warn';
2641
+ risk = volatility > 0.4 ? 'Elevated' : 'Moderate'; riskCls = volatility > 0.4 ? 'warn' : 'warn';
2642
+ } else if (score > 0.35) {
2643
+ strength = 'Moderate'; strengthCls = 'warn';
2644
+ evidence = 'Mixed'; evidenceCls = 'warn';
2645
+ risk = 'Elevated'; riskCls = 'bad';
2646
+ } else {
2647
+ strength = 'Weak'; strengthCls = 'bad';
2648
+ evidence = 'Thin'; evidenceCls = 'bad';
2649
+ risk = 'High'; riskCls = 'bad';
2650
+ }
2651
+
2652
+ return { score: score, strength: strength, strengthCls: strengthCls, evidence: evidence, evidenceCls: evidenceCls, risk: risk, riskCls: riskCls };
2653
+ }
2654
+
2655
+ function translateBehaviorNarrative(reaction, verdict) {
2656
+ // Convert verdict+action into before → after narrative — no system words
2657
+ var status = verdict ? verdict.status : 'ALLOW';
2658
+ var action = reaction || 'acted';
2659
+ if (status === 'BLOCK') {
2660
+ return 'abandoned ' + action + ' and switched to a safer strategy';
2661
+ }
2662
+ if (status === 'MODIFY' || status === 'PAUSE') {
2663
+ return 'scaled back ' + action + ' after early resistance';
2664
+ }
2665
+ return action;
2666
+ }
2667
+
2668
+ function generateBehaviorShifts(shiftData, bLog) {
2669
+ // Quantified behavioral shifts in human language
2670
+ var sentences = [];
2671
+ var total = shiftData.total || bLog.length || 0;
2672
+ if (total === 0) return sentences;
2673
+
2674
+ // Group by what agents actually did — always anchor in before → after
2675
+ var keys = Object.keys(shiftData.shifts || {}).sort(function(a, b) { return shiftData.shifts[b] - shiftData.shifts[a]; });
2676
+ if (keys.length > 0 && total > 0) {
2677
+ keys.slice(0, 3).forEach(function(k) {
2678
+ var parts = k.split(': ');
2679
+ var action = parts[1] || parts[0];
2680
+ var count = shiftData.shifts[k];
2681
+ var pct = Math.round((count / total) * 100);
2682
+ // before → after: what they tried → what they did instead
2683
+ if (parts[0] === 'BLOCK') {
2684
+ sentences.push(pct + '% of agents (' + count + ') shifted from ' + action + ' to conservative strategies');
2685
+ } else {
2686
+ sentences.push(pct + '% of agents (' + count + ') reduced ' + action + ' after initial attempts failed');
2687
+ }
2688
+ });
2689
+ }
2690
+
2691
+ // Fallback from behavioral log — still before → after
2692
+ if (sentences.length === 0 && bLog.length > 0) {
2693
+ var blocked = bLog.filter(function(e) { return e.status === 'BLOCK'; }).length;
2694
+ var modified = bLog.filter(function(e) { return e.status === 'MODIFY' || e.status === 'PAUSE'; }).length;
2695
+ var proceeded = bLog.length - blocked - modified;
2696
+
2697
+ if (blocked > 0) {
2698
+ var bPct = Math.round((blocked / bLog.length) * 100);
2699
+ sentences.push(bPct + '% of agents (' + blocked + ') shifted from aggressive to conservative strategies');
2700
+ }
2701
+ if (modified > 0) {
2702
+ var mPct = Math.round((modified / bLog.length) * 100);
2703
+ sentences.push(mPct + '% of agents (' + modified + ') reduced position size after initial attempts failed');
2704
+ }
2705
+ if (proceeded > 0 && (blocked > 0 || modified > 0)) {
2706
+ var pPct = Math.round((proceeded / bLog.length) * 100);
2707
+ sentences.push(pPct + '% of agents (' + proceeded + ') maintained their original strategy throughout');
2708
+ }
2709
+ }
2710
+
2711
+ // Volatility insight — before → after framing
2712
+ if (shiftData.governedVol < shiftData.baselineVol && shiftData.baselineVol > 0) {
2713
+ var volDrop = Math.round((1 - shiftData.governedVol / shiftData.baselineVol) * 100);
2714
+ sentences.push('Uncertainty dropped ' + volDrop + '% as agents moved from exploration to caution');
2715
+ }
2716
+
2717
+ // Add emergent pattern insights
2718
+ if (shiftData.patterns && shiftData.patterns.length > 0) {
2719
+ shiftData.patterns.slice(0, 2).forEach(function(p) {
2720
+ sentences.push(p);
2721
+ });
2722
+ }
2723
+
2724
+ return sentences;
2725
+ }
2726
+
2727
+ function generateCausation(shiftData, bLog) {
2728
+ // Pure causation — why behavior changed — no governance terminology
2729
+ var causes = [];
2730
+ var total = shiftData.total || bLog.length || 0;
2731
+ if (total === 0) return causes;
2732
+
2733
+ var blockRatio = shiftData.blocks / total;
2734
+
2735
+ // No system words (feedback, evaluation, validation, review)
2736
+ // Only agent experience (failed attempts, uncertainty, risk, delay)
2737
+ if (blockRatio > 0.4) {
2738
+ causes.push('Early aggressive attempts failed, forcing agents to rethink their strategy');
2739
+ } else if (blockRatio > 0.1) {
2740
+ causes.push('Some agents hit unexpected resistance and pulled back');
2741
+ }
2742
+
2743
+ if (shiftData.governedVol < shiftData.baselineVol) {
2744
+ causes.push('Uncertainty dropped as agents stopped experimenting and committed to safer positions');
2745
+ }
2746
+
2747
+ if (shiftData.patterns && shiftData.patterns.length > 0) {
2748
+ if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('hold') || p.toLowerCase().includes('caution'); })) {
2749
+ causes.push('Risk became too visible — agents chose to wait rather than act');
2750
+ }
2751
+ if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('coordination') || p.toLowerCase().includes('coordinated'); })) {
2752
+ causes.push('Agents independently converged on similar strategies under shared pressure');
2753
+ }
2754
+ }
2755
+
2756
+ // Detect adaptation pattern from behavior log
2757
+ if (bLog.length >= 3) {
2758
+ var recent = bLog.slice(-6);
2759
+ var earlyBlocks = recent.slice(0, 3).filter(function(e) { return e.status === 'BLOCK'; }).length;
2760
+ var lateAllows = recent.slice(-3).filter(function(e) { return e.status === 'ALLOW'; }).length;
2761
+ if (earlyBlocks >= 2 && lateAllows >= 2) {
2762
+ causes.push('Agents became more cautious after early attempts failed');
2763
+ }
2764
+ }
2765
+
2766
+ if (causes.length === 0) {
2767
+ causes.push('Agents held steady — no major shifts in strategy throughout the simulation');
2768
+ }
2769
+
2770
+ return causes;
2771
+ }
2772
+
1933
2773
  // ============================================
1934
2774
  // STATE
1935
2775
  // ============================================
@@ -1939,12 +2779,12 @@ let narratives = {};
1939
2779
  let currentWorld = null;
1940
2780
  let injectedEvents = [];
1941
2781
  let totalInterventions = 0;
1942
- let baselineImpacts = [];
1943
- let governedImpacts = [];
1944
- let chartLabels = [];
1945
- let chart = null;
2782
+ let totalActions = 0;
1946
2783
  let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
1947
2784
  let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
2785
+ let behaviorLog = []; // { agent, action, status, reason, ts }
2786
+ let latestStability = 0;
2787
+ let latestVolatility = 0;
1948
2788
 
1949
2789
  const statusEl = document.getElementById('status');
1950
2790
  const worldSelect = document.getElementById('world-select');
@@ -1963,6 +2803,79 @@ const activeInvEl = document.getElementById('active-invariants');
1963
2803
  const engineSelect = document.getElementById('engine-select');
1964
2804
  const engineStatusEl = document.getElementById('engine-status');
1965
2805
  const traceSourceEl = document.getElementById('trace-source');
2806
+ const outcomeStatementEl = document.getElementById('outcome-statement');
2807
+ const confidenceGridEl = document.getElementById('confidence-grid');
2808
+ const outcomeContextEl = document.getElementById('outcome-context');
2809
+ const behaviorShiftsEl = document.getElementById('behavior-shifts');
2810
+ const activityTimelineEl = document.getElementById('activity-timeline');
2811
+ const activityToggleEl = document.getElementById('activity-toggle');
2812
+ const whyContentEl = document.getElementById('why-content');
2813
+
2814
+ // Audit + Export
2815
+ function openAudit() {
2816
+ // Populate audit content from current data
2817
+ var rulesEl = document.getElementById('audit-rules-content');
2818
+ rulesEl.innerHTML = activeInvEl.innerHTML || '<div style="font-size:11px;color:#666">No rules loaded</div>';
2819
+
2820
+ var verdictsEl = document.getElementById('audit-verdicts-content');
2821
+ var vHtml = '';
2822
+ behaviorLog.forEach(function(e) {
2823
+ vHtml += '<div style="font-size:10px;padding:2px 0;display:flex;gap:8px;border-bottom:1px solid var(--border)">' +
2824
+ '<span style="color:var(--blue);min-width:100px">' + e.agent + '</span>' +
2825
+ '<span style="flex:1;color:var(--text-secondary)">' + e.action + '</span>' +
2826
+ '<span style="font-size:9px;font-weight:600;padding:0 4px;border-radius:2px;' +
2827
+ (e.status === 'BLOCK' ? 'background:var(--red-bg);color:var(--red)' : e.status === 'ALLOW' ? 'background:var(--green-bg);color:var(--green)' : 'background:var(--yellow-bg);color:var(--yellow)') + '">' + e.status + '</span>' +
2828
+ (e.reason ? '<span style="font-size:9px;color:var(--text-faint)">' + e.reason + '</span>' : '') +
2829
+ '</div>';
2830
+ });
2831
+ verdictsEl.innerHTML = vHtml || '<div style="font-size:11px;color:#666">No verdict data</div>';
2832
+
2833
+ var traceEl = document.getElementById('audit-trace-content');
2834
+ traceEl.innerHTML = logEl.innerHTML || '<div style="font-size:11px;color:#666">No trace data</div>';
2835
+
2836
+ document.getElementById('audit-overlay').classList.add('open');
2837
+ }
2838
+
2839
+ function closeAudit() {
2840
+ document.getElementById('audit-overlay').classList.remove('open');
2841
+ }
2842
+
2843
+ function exportPDF() {
2844
+ window.print();
2845
+ }
2846
+
2847
+ function exportCSV() {
2848
+ if (behaviorLog.length === 0) { alert('No data to export yet'); return; }
2849
+ var csv = 'Agent,Action,Behavior,Round\\n';
2850
+ behaviorLog.forEach(function(e) {
2851
+ var behavior = translateBehaviorNarrative(e.action, { status: e.status });
2852
+ csv += '"' + e.agent + '","' + e.action + '","' + behavior + '","' + (e.round || '') + '"\\n';
2853
+ });
2854
+ var blob = new Blob([csv], { type: 'text/csv' });
2855
+ var url = URL.createObjectURL(blob);
2856
+ var a = document.createElement('a');
2857
+ a.href = url;
2858
+ a.download = 'simulation-behavior-' + new Date().toISOString().slice(0, 10) + '.csv';
2859
+ a.click();
2860
+ URL.revokeObjectURL(url);
2861
+ }
2862
+
2863
+ 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); });
2869
+
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';
2874
+
2875
+ navigator.clipboard.writeText(text).then(function() {
2876
+ alert('Summary copied to clipboard');
2877
+ });
2878
+ }
1966
2879
 
1967
2880
  // ============================================
1968
2881
  // INIT — Load worlds, scenarios, narratives, adapters
@@ -2018,6 +2931,9 @@ async function init() {
2018
2931
  // Load saved variants
2019
2932
  await loadVariants();
2020
2933
 
2934
+ // Populate base world selector for custom rules mode
2935
+ populateBaseWorldSelect();
2936
+
2021
2937
  // Connect SSE
2022
2938
  connectSSE();
2023
2939
  }
@@ -2130,10 +3046,6 @@ runBtn.addEventListener('click', async () => {
2130
3046
 
2131
3047
  // Reset viewer state
2132
3048
  totalInterventions = 0;
2133
- baselineImpacts = [];
2134
- governedImpacts = [];
2135
- chartLabels = [];
2136
- if (chart) { chart.destroy(); chart = null; }
2137
3049
  agentsEl.innerHTML = '';
2138
3050
  logEl.innerHTML = '';
2139
3051
  document.getElementById('m-stability').textContent = '--';
@@ -2201,28 +3113,7 @@ function connectSSE() {
2201
3113
  }
2202
3114
 
2203
3115
  function initChart() {
2204
- if (typeof Chart === 'undefined') return;
2205
- const ctx = document.getElementById('chart');
2206
- chart = new Chart(ctx, {
2207
- type: 'line',
2208
- data: {
2209
- labels: chartLabels,
2210
- datasets: [
2211
- { label: 'Baseline', data: baselineImpacts, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
2212
- { label: 'Governed', data: governedImpacts, borderColor: '#4ade80', backgroundColor: 'rgba(74,222,128,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
2213
- ]
2214
- },
2215
- options: {
2216
- animation: { duration: 400 },
2217
- responsive: true,
2218
- maintainAspectRatio: false,
2219
- plugins: { legend: { labels: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888', font: { family: 'monospace', size: 10 } } } },
2220
- scales: {
2221
- x: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' } },
2222
- y: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' }, min: -1, max: 1 }
2223
- }
2224
- }
2225
- });
3116
+ // Chart removed no longer needed in outcome-first view
2226
3117
  }
2227
3118
 
2228
3119
  function addLog(msg, cls) {
@@ -2333,6 +3224,84 @@ function renderAgents(reactions) {
2333
3224
  agentsEl.innerHTML = html;
2334
3225
  }
2335
3226
 
3227
+ // ============================================
3228
+ // PANEL RENDERING — Outcome / Behavior / Why
3229
+ // ============================================
3230
+
3231
+ function recordBehavior(reactions, round) {
3232
+ if (!reactions || !reactions.length) return;
3233
+ reactions.forEach(function(r) {
3234
+ var status = r.verdict ? r.verdict.status : 'ALLOW';
3235
+ var reason = r.verdict ? (r.verdict.reason || '') : '';
3236
+ behaviorLog.push({
3237
+ agent: r.stakeholder_id,
3238
+ action: r.reaction || 'acted',
3239
+ status: status,
3240
+ reason: reason,
3241
+ round: round || 0,
3242
+ ts: Date.now(),
3243
+ });
3244
+ });
3245
+ }
3246
+
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 || '') : '');
3265
+ }
3266
+
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
+ }
3276
+
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;
3286
+ }
3287
+ }
3288
+
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;
3296
+ }
3297
+ }
3298
+
3299
+ function updateAllPanels() {
3300
+ updateOutcomePanel();
3301
+ updateBehaviorPanel();
3302
+ updateWhyPanel();
3303
+ }
3304
+
2336
3305
  function handleEvent(event) {
2337
3306
  if (event.type === 'meta') {
2338
3307
  statusEl.className = 'status live';
@@ -2415,44 +3384,59 @@ function handleEvent(event) {
2415
3384
  '<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
2416
3385
  '</div>';
2417
3386
  }).join('');
2418
- initChart();
2419
3387
  }
2420
3388
 
2421
3389
  if (event.type === 'round') {
2422
- if (event.phase === 'baseline') {
2423
- chartLabels.push('R' + event.round);
2424
- baselineImpacts.push(event.avgImpact);
2425
- } else {
2426
- governedImpacts.push(event.avgImpact);
2427
- }
2428
- if (chart) chart.update();
3390
+ const isBridge = event.reactions && event.reactions[0] && event.reactions[0].trigger === 'bridge';
2429
3391
 
2430
- renderAgents(event.reactions);
2431
- document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
2432
- document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
2433
- document.getElementById('m-volatility').parentElement.className = 'metric-box ' + (event.maxVolatility > 0.6 ? 'bad' : event.maxVolatility > 0.4 ? 'warn' : 'good');
3392
+ // Record behavior data (no governance display)
3393
+ recordBehavior(event.reactions, event.round);
3394
+ renderAgents(event.reactions); // hidden, kept for audit
2434
3395
 
3396
+ // Track totals
3397
+ totalActions += event.reactions ? event.reactions.length : 0;
2435
3398
  totalInterventions += event.interventionCount;
3399
+ latestVolatility = event.maxVolatility || 0;
3400
+
3401
+ // Update hidden data elements for audit
3402
+ if (isBridge) {
3403
+ document.getElementById('m-round').textContent = event.round + ' evals';
3404
+ } else {
3405
+ document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
3406
+ }
3407
+ document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
2436
3408
  document.getElementById('m-interventions').textContent = totalInterventions;
2437
3409
 
2438
- // Track system shifts for the card
3410
+ // Track system shifts (data collection, not displayed)
2439
3411
  trackShift(event);
2440
3412
 
2441
- // Render structured trace entry instead of flat log line
3413
+ // Update all visible panels
3414
+ updateAllPanels();
3415
+
3416
+ // Add trace entry for audit
2442
3417
  addTraceRound(event);
2443
3418
  }
2444
3419
 
3420
+ // Bridge metrics — updates stability from external /api/evaluate calls
3421
+ if (event.type === 'bridge_metrics') {
3422
+ latestStability = event.stability;
3423
+ document.getElementById('m-stability').textContent = (event.stability * 100).toFixed(0) + '%';
3424
+ updateAllPanels();
3425
+ }
3426
+
2445
3427
  if (event.type === 'complete') {
2446
3428
  statusEl.className = 'status complete';
2447
3429
  statusEl.textContent = 'COMPLETE';
2448
3430
  const r = event.result;
2449
3431
  if (r.governed) {
3432
+ latestStability = r.governed.metrics.stabilityScore;
3433
+ latestVolatility = r.governed.metrics.maxVolatility || latestVolatility;
2450
3434
  document.getElementById('m-stability').textContent = (r.governed.metrics.stabilityScore * 100).toFixed(0) + '%';
2451
- document.getElementById('m-stability').parentElement.className = 'metric-box ' + (r.governed.metrics.stabilityScore > 0.7 ? 'good' : 'warn');
2452
- addLog('Complete. Governance effectiveness: ' + (r.comparison.governanceEffectiveness * 100).toFixed(0) + '%');
3435
+ addLog('Simulation complete');
2453
3436
  renderSystemShift(r);
2454
3437
  renderRuleImpacts(r);
2455
3438
  renderEnforcementClassification(r.enforcementClassification || []);
3439
+ updateAllPanels();
2456
3440
  lastSimResult = {
2457
3441
  stability: r.governed.metrics.stabilityScore,
2458
3442
  volatility: r.governed.metrics.maxVolatility,
@@ -2531,20 +3515,37 @@ const ssImpactsEl = document.getElementById('ss-impacts');
2531
3515
  const ssNarrativeEl = document.getElementById('ss-narrative');
2532
3516
 
2533
3517
  // Raw detail toggle
2534
- document.getElementById('ss-raw-toggle').addEventListener('click', function() {
2535
- this.classList.toggle('open');
2536
- document.getElementById('ss-raw-detail').classList.toggle('open');
2537
- });
3518
+ var ssRawToggle = document.getElementById('ss-raw-toggle');
3519
+ if (ssRawToggle) {
3520
+ ssRawToggle.addEventListener('click', function() {
3521
+ this.classList.toggle('open');
3522
+ document.getElementById('ss-raw-detail').classList.toggle('open');
3523
+ });
3524
+ }
2538
3525
 
2539
3526
  let shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
2540
3527
 
2541
3528
  function resetShiftTracker() {
2542
3529
  shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
2543
- ssCard.classList.remove('visible');
3530
+ if (ssCard) ssCard.classList.remove('visible');
2544
3531
  var rawToggle = document.getElementById('ss-raw-toggle');
2545
3532
  var rawDetail = document.getElementById('ss-raw-detail');
2546
3533
  if (rawToggle) rawToggle.classList.remove('open');
2547
3534
  if (rawDetail) rawDetail.classList.remove('open');
3535
+ // Reset behavior tracking
3536
+ behaviorLog = [];
3537
+ totalActions = 0;
3538
+ totalInterventions = 0;
3539
+ latestStability = 0;
3540
+ latestVolatility = 0;
3541
+ // Reset visible panels
3542
+ if (outcomeStatementEl) outcomeStatementEl.innerHTML = '<span class="outcome-empty">Run a simulation or connect an agent to see outcomes</span>';
3543
+ if (confidenceGridEl) confidenceGridEl.style.display = 'none';
3544
+ if (outcomeContextEl) outcomeContextEl.textContent = '';
3545
+ if (behaviorShiftsEl) behaviorShiftsEl.innerHTML = '<div class="behavior-empty">Waiting for agent actions</div>';
3546
+ if (activityToggleEl) activityToggleEl.style.display = 'none';
3547
+ if (activityTimelineEl) { activityTimelineEl.innerHTML = ''; activityTimelineEl.classList.remove('open'); }
3548
+ if (whyContentEl) whyContentEl.innerHTML = '<div class="why-empty">Causation analysis appears after agent actions are evaluated</div>';
2548
3549
  }
2549
3550
 
2550
3551
  function trackShift(event) {
@@ -2587,6 +3588,7 @@ function trackShift(event) {
2587
3588
 
2588
3589
  function renderSystemShift(result) {
2589
3590
  if (shiftTracker.blocks === 0) return;
3591
+ if (!ssCard) return; // System shift card removed from visible UI
2590
3592
 
2591
3593
  var adaptRate = shiftTracker.total > 0 ? Math.round((shiftTracker.blocks / shiftTracker.total) * 100) : 0;
2592
3594
 
@@ -2877,18 +3879,6 @@ function applyTheme(theme) {
2877
3879
  themeToggleBtn.textContent = 'Light Mode';
2878
3880
  }
2879
3881
  localStorage.setItem('nv-theme', theme);
2880
- // Update chart colors if chart exists
2881
- if (chart && chart.options) {
2882
- const gridColor = theme === 'light' ? '#d4d4d4' : '#2a2a2a';
2883
- const tickColor = theme === 'light' ? '#666' : '#888';
2884
- const legendColor = theme === 'light' ? '#444' : '#888';
2885
- chart.options.scales.x.ticks.color = tickColor;
2886
- chart.options.scales.x.grid.color = gridColor;
2887
- chart.options.scales.y.ticks.color = tickColor;
2888
- chart.options.scales.y.grid.color = gridColor;
2889
- chart.options.plugins.legend.labels.color = legendColor;
2890
- chart.update();
2891
- }
2892
3882
  }
2893
3883
  themeToggleBtn.addEventListener('click', () => {
2894
3884
  const current = document.body.classList.contains('light') ? 'light' : 'dark';
@@ -2898,6 +3888,257 @@ themeToggleBtn.addEventListener('click', () => {
2898
3888
  const savedTheme = localStorage.getItem('nv-theme');
2899
3889
  if (savedTheme) applyTheme(savedTheme);
2900
3890
 
3891
+ // ============================================
3892
+ // WORLD SOURCE SWITCHING
3893
+ // ============================================
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
+
3931
+ // ============================================
3932
+ // WORLD ACTION BAR
3933
+ // ============================================
3934
+
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
+ // ============================================
4014
+ // WORLD FILE UPLOAD / PASTE
4015
+ // ============================================
4016
+ const uploadZone = document.getElementById('upload-zone');
4017
+ const uploadFileInput = document.getElementById('upload-file-input');
4018
+ const uploadBrowseBtn = document.getElementById('upload-browse-btn');
4019
+ const worldJsonInput = document.getElementById('world-json-input');
4020
+ const loadWorldBtn = document.getElementById('load-world-btn');
4021
+ const uploadStatusEl = document.getElementById('upload-status');
4022
+
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(); });
4041
+
4042
+ function readWorldFile(file) {
4043
+ const reader = new FileReader();
4044
+ 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';
4048
+ };
4049
+ reader.readAsText(file);
4050
+ }
4051
+
4052
+ // Load into Runtime
4053
+ loadWorldBtn.addEventListener('click', async () => {
4054
+ const jsonText = worldJsonInput.value.trim();
4055
+ if (!jsonText) {
4056
+ uploadStatusEl.textContent = 'Paste or upload a world file first.';
4057
+ uploadStatusEl.className = 'rule-status error';
4058
+ return;
4059
+ }
4060
+
4061
+ 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';
4067
+ return;
4068
+ }
4069
+
4070
+ // Normalize: if the JSON is { world: {...} } or just {...}
4071
+ const worldPayload = worldData.world || worldData;
4072
+
4073
+ loadWorldBtn.textContent = 'Loading...';
4074
+ loadWorldBtn.disabled = true;
4075
+
4076
+ try {
4077
+ const resp = await fetch('/api/load-world-file', {
4078
+ method: 'POST',
4079
+ headers: { 'Content-Type': 'application/json' },
4080
+ body: JSON.stringify({ world: worldPayload }),
4081
+ });
4082
+ const result = await resp.json();
4083
+
4084
+ if (result.error) {
4085
+ uploadStatusEl.textContent = result.error;
4086
+ uploadStatusEl.className = 'rule-status error';
4087
+ } else {
4088
+ uploadStatusEl.textContent = result.message;
4089
+ uploadStatusEl.className = 'rule-status success';
4090
+
4091
+ // 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 =>
4103
+ '<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
+ ).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
+ }
4133
+ } catch (err) {
4134
+ uploadStatusEl.textContent = 'Error: ' + err.message;
4135
+ uploadStatusEl.className = 'rule-status error';
4136
+ }
4137
+
4138
+ loadWorldBtn.textContent = 'Load into Runtime';
4139
+ loadWorldBtn.disabled = false;
4140
+ });
4141
+
2901
4142
  // ============================================
2902
4143
  // PLAIN-ENGLISH RULE EDITOR
2903
4144
  // ============================================
@@ -2920,7 +4161,7 @@ parseRulesBtn.addEventListener('click', async () => {
2920
4161
  const resp = await fetch('/api/parse-rules', {
2921
4162
  method: 'POST',
2922
4163
  headers: { 'Content-Type': 'application/json' },
2923
- body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
4164
+ body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'social_simulation' }),
2924
4165
  });
2925
4166
  const data = await resp.json();
2926
4167
 
@@ -2947,25 +4188,84 @@ parseRulesBtn.addEventListener('click', async () => {
2947
4188
  }).join('');
2948
4189
 
2949
4190
  if (parsedRuleData.length > 0) {
2950
- parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation</button>';
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>';
2951
4193
  document.getElementById('apply-rules-btn').addEventListener('click', async () => {
2952
4194
  try {
2953
- const applyResp = await fetch('/api/apply-rules', {
2954
- method: 'POST',
2955
- headers: { 'Content-Type': 'application/json' },
2956
- body: JSON.stringify({ rules: parsedRuleData, worldId: currentWorld ? currentWorld.id : 'trading' }),
2957
- });
2958
- const applyData = await applyResp.json();
2959
- if (applyData.status === 'applied') {
2960
- ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
2961
- ruleStatusEl.className = 'rule-status success';
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
+ }
2962
4262
  }
2963
4263
  } catch (err) {
2964
4264
  ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
2965
4265
  ruleStatusEl.className = 'rule-status error';
2966
4266
  }
2967
4267
  });
2968
- ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click Apply.';
4268
+ ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
2969
4269
  ruleStatusEl.className = 'rule-status success';
2970
4270
  }
2971
4271
  }