@neuroverseos/nv-sim 0.1.2 → 0.1.6
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.
- package/README.md +376 -66
- package/dist/adapters/mirofish.js +461 -0
- package/dist/adapters/scienceclaw.js +750 -0
- package/dist/assets/index-CHmUN8s0.js +532 -0
- package/dist/assets/index-DWgMnB7I.css +1 -0
- package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
- package/dist/assets/reportEngine-BVdQ2_nW.js +1 -0
- package/dist/components/ConstraintsPanel.js +11 -0
- package/dist/components/StakeholderBuilder.js +32 -0
- package/dist/components/ui/badge.js +24 -0
- package/dist/components/ui/button.js +70 -0
- package/dist/components/ui/card.js +57 -0
- package/dist/components/ui/input.js +44 -0
- package/dist/components/ui/label.js +45 -0
- package/dist/components/ui/select.js +70 -0
- package/dist/engine/aiProvider.js +681 -0
- package/dist/engine/auditTrace.js +352 -0
- package/dist/engine/behavioralAnalysis.js +605 -0
- package/dist/engine/cli.js +1408 -299
- package/dist/engine/dynamicsGovernance.js +588 -0
- package/dist/engine/fullGovernedLoop.js +367 -0
- package/dist/engine/governance.js +8 -3
- package/dist/engine/governedSimulation.js +114 -17
- package/dist/engine/index.js +56 -1
- package/dist/engine/liveAdapter.js +342 -0
- package/dist/engine/liveVisualizer.js +3063 -0
- package/dist/engine/metrics/science.metrics.js +335 -0
- package/dist/engine/narrativeInjection.js +305 -0
- package/dist/engine/policyEnforcement.js +1611 -0
- package/dist/engine/policyEngine.js +799 -0
- package/dist/engine/primeRadiant.js +540 -0
- package/dist/engine/reasoningEngine.js +57 -3
- package/dist/engine/reportEngine.js +97 -0
- package/dist/engine/scenarioComparison.js +463 -0
- package/dist/engine/scenarioLibrary.js +231 -0
- package/dist/engine/swarmSimulation.js +54 -1
- package/dist/engine/worldComparison.js +358 -0
- package/dist/engine/worldStorage.js +232 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +23 -0
- package/dist/lib/reasoningEngine.js +290 -0
- package/dist/lib/simulationAdapter.js +686 -0
- package/dist/lib/swarmParser.js +291 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils.js +8 -0
- package/dist/placeholder.svg +1 -0
- package/dist/robots.txt +14 -0
- package/dist/runtime/govern.js +473 -0
- package/dist/runtime/index.js +75 -0
- package/dist/runtime/types.js +11 -0
- package/package.json +17 -12
- package/variants/.gitkeep +0 -0
|
@@ -0,0 +1,3063 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live Visualizer — Round-by-round simulation viewer
|
|
4
|
+
*
|
|
5
|
+
* Streams simulation data to a local browser dashboard via Server-Sent Events.
|
|
6
|
+
* Watch rules fire, agents react, and governance reshape the system in real time.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* nv-sim visualize trading --live
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.startInteractiveServer = startInteractiveServer;
|
|
46
|
+
const http = __importStar(require("http"));
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const swarmSimulation_1 = require("./swarmSimulation");
|
|
50
|
+
const worldBridge_1 = require("./worldBridge");
|
|
51
|
+
const auditTrace_1 = require("./auditTrace");
|
|
52
|
+
const narrativeInjection_1 = require("./narrativeInjection");
|
|
53
|
+
const scenarioLibrary_1 = require("./scenarioLibrary");
|
|
54
|
+
const liveAdapter_1 = require("./liveAdapter");
|
|
55
|
+
function getVariantsDir() {
|
|
56
|
+
return path.resolve(process.cwd(), "variants");
|
|
57
|
+
}
|
|
58
|
+
function ensureVariantsDir() {
|
|
59
|
+
const dir = getVariantsDir();
|
|
60
|
+
if (!fs.existsSync(dir)) {
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function slugify(name) {
|
|
65
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
66
|
+
}
|
|
67
|
+
function loadAllVariants() {
|
|
68
|
+
ensureVariantsDir();
|
|
69
|
+
const dir = getVariantsDir();
|
|
70
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith(".json")).sort();
|
|
71
|
+
return files.map(f => {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}).filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
function saveVariant(variant) {
|
|
81
|
+
ensureVariantsDir();
|
|
82
|
+
const filename = `${variant.id}.json`;
|
|
83
|
+
const filepath = path.join(getVariantsDir(), filename);
|
|
84
|
+
fs.writeFileSync(filepath, JSON.stringify(variant, null, 2), "utf-8");
|
|
85
|
+
return filepath;
|
|
86
|
+
}
|
|
87
|
+
function deleteVariant(variantId) {
|
|
88
|
+
const filepath = path.join(getVariantsDir(), `${variantId}.json`);
|
|
89
|
+
if (fs.existsSync(filepath)) {
|
|
90
|
+
fs.unlinkSync(filepath);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Start an interactive live server with world controls + live simulation.
|
|
97
|
+
*
|
|
98
|
+
* The UI sends POST /run-sim with world parameters.
|
|
99
|
+
* The server runs the simulation and streams results via SSE.
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* Parse a single plain-English rule into a guard definition.
|
|
103
|
+
* No LLM needed — pattern matching on common financial governance phrases.
|
|
104
|
+
*/
|
|
105
|
+
function parseNaturalLanguageRule(line, index) {
|
|
106
|
+
const lower = line.toLowerCase().trim();
|
|
107
|
+
if (!lower)
|
|
108
|
+
return null;
|
|
109
|
+
// Determine enforcement action
|
|
110
|
+
let enforcement = "block";
|
|
111
|
+
if (/^(allow|permit|enable)\b/.test(lower))
|
|
112
|
+
enforcement = "allow";
|
|
113
|
+
else if (/^(pause|review|flag|hold|require.*review|require.*approval)\b/.test(lower))
|
|
114
|
+
enforcement = "pause";
|
|
115
|
+
else if (/^(block|ban|prevent|prohibit|stop|forbid|disallow|no\b|don.t allow)\b/.test(lower))
|
|
116
|
+
enforcement = "block";
|
|
117
|
+
else if (/^limit\b/.test(lower))
|
|
118
|
+
enforcement = "block";
|
|
119
|
+
// Extract intent patterns from the rule text
|
|
120
|
+
const patterns = [];
|
|
121
|
+
const actionWords = {
|
|
122
|
+
"panic sell": ["panic_sell", "panic sell", "panic selling"],
|
|
123
|
+
"panic buy": ["panic_buy", "panic buy", "panic buying"],
|
|
124
|
+
"short sell": ["short", "short_sell", "short selling", "shorting"],
|
|
125
|
+
"leverage": ["increase_leverage", "increase leverage", "max leverage", "excessive leverage"],
|
|
126
|
+
"large trade": ["large_trade", "large trade", "bulk trade", "big trade"],
|
|
127
|
+
"aggressive": ["aggressive_buy", "aggressive buy", "aggressive_short", "aggressive short"],
|
|
128
|
+
"flash": ["flash_trade", "flash trade", "high_frequency"],
|
|
129
|
+
"margin": ["margin_call", "margin call", "increase_margin"],
|
|
130
|
+
"liquidat": ["liquidate", "liquidation", "force_liquidate"],
|
|
131
|
+
"hedge": ["hedge", "hedging", "hedge_position"],
|
|
132
|
+
"buy": ["buy", "aggressive_buy"],
|
|
133
|
+
"sell": ["sell", "selling"],
|
|
134
|
+
"trade": ["trade", "trading"],
|
|
135
|
+
"withdraw": ["withdraw", "withdrawal", "bank_run"],
|
|
136
|
+
"circuit breaker": ["circuit_breaker", "halt_trading", "trading_halt"],
|
|
137
|
+
};
|
|
138
|
+
for (const [keyword, intents] of Object.entries(actionWords)) {
|
|
139
|
+
if (lower.includes(keyword)) {
|
|
140
|
+
patterns.push(...intents);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Fallback: extract quoted terms or the main noun phrase
|
|
144
|
+
if (patterns.length === 0) {
|
|
145
|
+
const quoted = line.match(/"([^"]+)"/g);
|
|
146
|
+
if (quoted) {
|
|
147
|
+
quoted.forEach(q => {
|
|
148
|
+
const term = q.replace(/"/g, "").trim();
|
|
149
|
+
patterns.push(term, term.replace(/\s+/g, "_"));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Extract the action part after block/allow/pause
|
|
154
|
+
const actionPart = lower.replace(/^(block|ban|prevent|prohibit|stop|forbid|disallow|allow|permit|enable|pause|review|flag|hold|limit|no|don.t allow)\s+/i, "").trim();
|
|
155
|
+
if (actionPart) {
|
|
156
|
+
patterns.push(actionPart, actionPart.replace(/\s+/g, "_"));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Deduplicate
|
|
161
|
+
const uniquePatterns = [...new Set(patterns)];
|
|
162
|
+
if (uniquePatterns.length === 0)
|
|
163
|
+
return null;
|
|
164
|
+
return {
|
|
165
|
+
id: `custom-rule-${index}`,
|
|
166
|
+
description: line,
|
|
167
|
+
enforcement,
|
|
168
|
+
intent_patterns: uniquePatterns,
|
|
169
|
+
category: "custom",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function startInteractiveServer(port, onReady) {
|
|
173
|
+
const clients = new Set();
|
|
174
|
+
let isRunning = false;
|
|
175
|
+
// ── Persistent Audit Trail ──
|
|
176
|
+
// Every governance verdict is written to disk as JSONL
|
|
177
|
+
const auditTrail = new auditTrace_1.AuditTrail();
|
|
178
|
+
// Custom guards from plain-English rule editor (session-scoped)
|
|
179
|
+
const customGuards = [];
|
|
180
|
+
let currentSession = {
|
|
181
|
+
id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
182
|
+
startedAt: new Date().toISOString(),
|
|
183
|
+
world: "trading",
|
|
184
|
+
guardCount: 0,
|
|
185
|
+
evaluations: [],
|
|
186
|
+
};
|
|
187
|
+
// Session snapshots for multi-run comparison
|
|
188
|
+
const sessionHistory = [];
|
|
189
|
+
function synthesizeSessionReport() {
|
|
190
|
+
// Gather all sessions to report on (history + current if it has data)
|
|
191
|
+
const allSessions = [
|
|
192
|
+
...sessionHistory,
|
|
193
|
+
...(currentSession.evaluations.length > 0 ? [currentSession] : []),
|
|
194
|
+
];
|
|
195
|
+
if (allSessions.length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
sessionId: currentSession.id,
|
|
198
|
+
scenario: "Live governance session (no evaluations yet)",
|
|
199
|
+
runs: [],
|
|
200
|
+
divergence: {
|
|
201
|
+
stabilityTrend: [],
|
|
202
|
+
collapseTrend: [],
|
|
203
|
+
effectivenessTrend: [],
|
|
204
|
+
bestIteration: 0,
|
|
205
|
+
worstIteration: 0,
|
|
206
|
+
totalDivergence: 0,
|
|
207
|
+
narrative: "No evaluations recorded. Send actions to POST /api/evaluate to begin.",
|
|
208
|
+
},
|
|
209
|
+
recommendation: "Start sending agent actions to /api/evaluate to generate governance data.",
|
|
210
|
+
generatedAt: new Date().toISOString(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const runs = allSessions.map((sess, idx) => {
|
|
214
|
+
const evals = sess.evaluations;
|
|
215
|
+
const total = evals.length;
|
|
216
|
+
const blocked = evals.filter(e => e.decision === "BLOCK").length;
|
|
217
|
+
const modified = evals.filter(e => e.decision === "MODIFY").length;
|
|
218
|
+
const allowed = evals.filter(e => e.decision === "ALLOW").length;
|
|
219
|
+
// Synthesize metrics from real evaluation data
|
|
220
|
+
const blockRate = total > 0 ? blocked / total : 0;
|
|
221
|
+
const interventionRate = total > 0 ? (blocked + modified) / total : 0;
|
|
222
|
+
// Stability: higher when governance is actively catching harmful actions
|
|
223
|
+
// If nothing is blocked, either the system is clean OR governance is too weak
|
|
224
|
+
const stabilityScore = total > 0
|
|
225
|
+
? Math.min(0.95, 0.4 + interventionRate * 0.4 + (allowed / Math.max(1, total)) * 0.2)
|
|
226
|
+
: 0.5;
|
|
227
|
+
// Collapse probability: lower when more harmful actions are caught
|
|
228
|
+
const collapseProbability = total > 0
|
|
229
|
+
? Math.max(0.02, 0.6 - interventionRate * 0.5 - blockRate * 0.15)
|
|
230
|
+
: 0.5;
|
|
231
|
+
// Governance effectiveness: composite of intervention quality
|
|
232
|
+
const governanceEffectiveness = total > 0
|
|
233
|
+
? Math.min(0.95, interventionRate * 0.6 + blockRate * 0.3 + (total > 10 ? 0.1 : 0))
|
|
234
|
+
: 0;
|
|
235
|
+
// Unique rules that fired
|
|
236
|
+
const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
|
|
237
|
+
// Unique actors and actions
|
|
238
|
+
const uniqueActors = [...new Set(evals.map(e => e.actor))];
|
|
239
|
+
const uniqueActions = [...new Set(evals.map(e => e.action))];
|
|
240
|
+
return {
|
|
241
|
+
iteration: idx + 1,
|
|
242
|
+
worldName: sess.world || "live-session",
|
|
243
|
+
ruleCount: sess.guardCount,
|
|
244
|
+
gateCount: 0,
|
|
245
|
+
metrics: {
|
|
246
|
+
avgImpact: interventionRate > 0 ? -(interventionRate * 0.5) : 0.1,
|
|
247
|
+
collapseProbability,
|
|
248
|
+
stabilityScore,
|
|
249
|
+
coalitionRisks: 0,
|
|
250
|
+
polarizationEvents: 0,
|
|
251
|
+
peakNegativeSentiment: blockRate > 0.3 ? -0.6 : -0.2,
|
|
252
|
+
consensusRounds: 0,
|
|
253
|
+
maxVolatility: blockRate > 0.2 ? 0.7 : 0.3,
|
|
254
|
+
},
|
|
255
|
+
comparison: {
|
|
256
|
+
collapseReduction: collapseProbability < 0.5 ? (0.6 - collapseProbability) : 0,
|
|
257
|
+
stabilityImprovement: stabilityScore > 0.5 ? (stabilityScore - 0.4) : 0,
|
|
258
|
+
volatilityReduction: interventionRate * 0.5,
|
|
259
|
+
coalitionRiskReduction: 0,
|
|
260
|
+
governanceEffectiveness,
|
|
261
|
+
narrative: total > 0
|
|
262
|
+
? `${total} actions evaluated: ${blocked} blocked, ${modified} modified, ${allowed} allowed across ${uniqueActors.length} agent(s) performing ${uniqueActions.length} action type(s).`
|
|
263
|
+
: "No evaluations in this session.",
|
|
264
|
+
},
|
|
265
|
+
governanceStats: {
|
|
266
|
+
engineLoaded: true,
|
|
267
|
+
totalEvaluations: total,
|
|
268
|
+
verdicts: { allow: allowed, block: blocked, pause: modified },
|
|
269
|
+
rulesFired: triggeredRules.length,
|
|
270
|
+
worldCollapsed: false,
|
|
271
|
+
finalViability: stabilityScore > 0.6 ? "stable" : stabilityScore > 0.4 ? "at-risk" : "critical",
|
|
272
|
+
invariantsChecked: sess.guardCount,
|
|
273
|
+
triggeredGuards: triggeredRules,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
// Compute rule changes between sessions
|
|
278
|
+
for (let i = 1; i < runs.length; i++) {
|
|
279
|
+
const prev = allSessions[i - 1];
|
|
280
|
+
const curr = allSessions[i];
|
|
281
|
+
if (prev.guardCount !== curr.guardCount) {
|
|
282
|
+
runs[i].ruleChanges = {
|
|
283
|
+
added: [],
|
|
284
|
+
removed: [],
|
|
285
|
+
gatesAdded: [],
|
|
286
|
+
gatesRemoved: [],
|
|
287
|
+
thesisChanged: prev.world !== curr.world,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Divergence analysis
|
|
292
|
+
const stabilityTrend = runs.map(r => r.metrics.stabilityScore);
|
|
293
|
+
const collapseTrend = runs.map(r => r.metrics.collapseProbability);
|
|
294
|
+
const effectivenessTrend = runs.map(r => r.comparison.governanceEffectiveness);
|
|
295
|
+
let bestIteration = 1;
|
|
296
|
+
let worstIteration = 1;
|
|
297
|
+
let bestScore = -Infinity;
|
|
298
|
+
let worstScore = Infinity;
|
|
299
|
+
for (const run of runs) {
|
|
300
|
+
if (run.comparison.governanceEffectiveness > bestScore) {
|
|
301
|
+
bestScore = run.comparison.governanceEffectiveness;
|
|
302
|
+
bestIteration = run.iteration;
|
|
303
|
+
}
|
|
304
|
+
if (run.comparison.governanceEffectiveness < worstScore) {
|
|
305
|
+
worstScore = run.comparison.governanceEffectiveness;
|
|
306
|
+
worstIteration = run.iteration;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
let totalDivergence = 0;
|
|
310
|
+
for (let i = 1; i < runs.length; i++) {
|
|
311
|
+
totalDivergence += Math.abs(runs[i].metrics.stabilityScore - runs[i - 1].metrics.stabilityScore);
|
|
312
|
+
totalDivergence += Math.abs(runs[i].metrics.collapseProbability - runs[i - 1].metrics.collapseProbability);
|
|
313
|
+
totalDivergence += Math.abs(runs[i].comparison.governanceEffectiveness - runs[i - 1].comparison.governanceEffectiveness);
|
|
314
|
+
}
|
|
315
|
+
// Build narrative
|
|
316
|
+
const totalEvals = allSessions.reduce((sum, s) => sum + s.evaluations.length, 0);
|
|
317
|
+
const totalBlocked = allSessions.reduce((sum, s) => sum + s.evaluations.filter(e => e.decision === "BLOCK").length, 0);
|
|
318
|
+
const narrativeParts = [];
|
|
319
|
+
narrativeParts.push(`${allSessions.length} session(s), ${totalEvals} total evaluations, ${totalBlocked} blocked.`);
|
|
320
|
+
if (runs.length > 1) {
|
|
321
|
+
const stabDelta = stabilityTrend[stabilityTrend.length - 1] - stabilityTrend[0];
|
|
322
|
+
if (stabDelta > 0.05)
|
|
323
|
+
narrativeParts.push(`Stability improved ${(stabDelta * 100).toFixed(0)}pp across sessions.`);
|
|
324
|
+
narrativeParts.push(`Best outcome: session ${bestIteration}.`);
|
|
325
|
+
}
|
|
326
|
+
const best = runs[bestIteration - 1];
|
|
327
|
+
const recommendation = runs.length === 1
|
|
328
|
+
? `Single session: ${best.governanceStats.totalEvaluations} evaluations, ${best.governanceStats.verdicts.block} blocked. Apply different rules and reset to compare.`
|
|
329
|
+
: `Best session: #${bestIteration} ("${best.worldName}") — ${(best.comparison.governanceEffectiveness * 100).toFixed(0)}% effectiveness, ${(best.metrics.stabilityScore * 100).toFixed(0)}% stability.`;
|
|
330
|
+
return {
|
|
331
|
+
sessionId: currentSession.id,
|
|
332
|
+
scenario: `Live governance — ${currentSession.world}`,
|
|
333
|
+
runs,
|
|
334
|
+
divergence: {
|
|
335
|
+
stabilityTrend,
|
|
336
|
+
collapseTrend,
|
|
337
|
+
effectivenessTrend,
|
|
338
|
+
bestIteration,
|
|
339
|
+
worstIteration,
|
|
340
|
+
totalDivergence: Number(totalDivergence.toFixed(3)),
|
|
341
|
+
narrative: narrativeParts.join(" "),
|
|
342
|
+
},
|
|
343
|
+
recommendation,
|
|
344
|
+
generatedAt: new Date().toISOString(),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function broadcast(event) {
|
|
348
|
+
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
349
|
+
for (const client of clients) {
|
|
350
|
+
client.write(data);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function readBody(req) {
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
let body = "";
|
|
356
|
+
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
357
|
+
req.on("end", () => resolve(body));
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function jsonResponse(res, status, data) {
|
|
361
|
+
res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
362
|
+
res.end(JSON.stringify(data));
|
|
363
|
+
}
|
|
364
|
+
async function runSimulation(config) {
|
|
365
|
+
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
366
|
+
const { runGovernedComparison } = await Promise.resolve().then(() => __importStar(require("./governedSimulation")));
|
|
367
|
+
const resolved = resolveWorld(config.worldId);
|
|
368
|
+
const rounds = config.rounds ?? resolved.swarm.rounds ?? 5;
|
|
369
|
+
// Apply state variable overrides to world definition
|
|
370
|
+
const world = { ...resolved.world };
|
|
371
|
+
if (config.stateOverrides) {
|
|
372
|
+
const updatedVars = world.state_variables.map(sv => {
|
|
373
|
+
if (config.stateOverrides && sv.id in config.stateOverrides) {
|
|
374
|
+
return { ...sv, default_value: config.stateOverrides[sv.id] };
|
|
375
|
+
}
|
|
376
|
+
return sv;
|
|
377
|
+
});
|
|
378
|
+
world.state_variables = updatedVars;
|
|
379
|
+
}
|
|
380
|
+
// Inject custom rules as first-class invariants (not just guards)
|
|
381
|
+
// This ensures user rules affect BOTH governance paths:
|
|
382
|
+
// 1. Guard engine (intent-level blocking) — already wired via customGuards[]
|
|
383
|
+
// 2. Invariant engine (system-level stability shaping) — wired here
|
|
384
|
+
if (customGuards.length > 0) {
|
|
385
|
+
const customInvariants = customGuards.map(cg => ({
|
|
386
|
+
id: cg.id,
|
|
387
|
+
description: cg.description,
|
|
388
|
+
enforceable: cg.enforcement === "block",
|
|
389
|
+
}));
|
|
390
|
+
world.invariants = [...world.invariants, ...customInvariants];
|
|
391
|
+
}
|
|
392
|
+
// Resolve narrative events
|
|
393
|
+
let narrativeEvents = [];
|
|
394
|
+
if (config.scenarioId && scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]) {
|
|
395
|
+
narrativeEvents = (0, scenarioLibrary_1.resolveScenarioEvents)(scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]);
|
|
396
|
+
}
|
|
397
|
+
else if (config.injectEvents && config.injectEvents.length > 0) {
|
|
398
|
+
narrativeEvents = (0, narrativeInjection_1.parseInjectArgs)(["--inject", config.injectEvents.join(",")]);
|
|
399
|
+
}
|
|
400
|
+
const request = {
|
|
401
|
+
scenario: resolved.scenario,
|
|
402
|
+
stakeholders: resolved.stakeholders,
|
|
403
|
+
assumptions: resolved.assumptions,
|
|
404
|
+
constraints: resolved.constraints,
|
|
405
|
+
depth: resolved.depth,
|
|
406
|
+
swarm: { ...resolved.swarm, rounds },
|
|
407
|
+
};
|
|
408
|
+
// Send meta event
|
|
409
|
+
broadcast({
|
|
410
|
+
type: "meta",
|
|
411
|
+
source: "nv-sim",
|
|
412
|
+
scenario: resolved.scenario,
|
|
413
|
+
worldThesis: world.thesis,
|
|
414
|
+
agents: resolved.stakeholders.map(s => s.id),
|
|
415
|
+
invariants: world.invariants.map(inv => ({
|
|
416
|
+
id: inv.id,
|
|
417
|
+
description: inv.description,
|
|
418
|
+
enforcement: inv.enforceable ? "full" : "advisory",
|
|
419
|
+
source: inv.id.startsWith("custom-rule-") ? "user" : "world",
|
|
420
|
+
})),
|
|
421
|
+
gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
|
|
422
|
+
narrativeEvents: narrativeEvents.map(e => ({ id: e.id, headline: e.headline, round: e.round, severity: e.severity })),
|
|
423
|
+
totalRounds: rounds,
|
|
424
|
+
});
|
|
425
|
+
// Run baseline
|
|
426
|
+
const baselineResult = await (0, swarmSimulation_1.runSwarmSimulation)(resolved.scenario, resolved.stakeholders, resolved.paths, { ...resolved.swarm, rounds });
|
|
427
|
+
for (const round of baselineResult.rounds) {
|
|
428
|
+
broadcast({
|
|
429
|
+
type: "round",
|
|
430
|
+
round: round.round,
|
|
431
|
+
totalRounds: rounds,
|
|
432
|
+
phase: "baseline",
|
|
433
|
+
reactions: round.reactions.map(r => ({
|
|
434
|
+
stakeholder_id: r.stakeholder_id,
|
|
435
|
+
reaction: r.reaction,
|
|
436
|
+
impact: r.impact,
|
|
437
|
+
confidence: r.confidence,
|
|
438
|
+
trigger: r.trigger,
|
|
439
|
+
})),
|
|
440
|
+
avgImpact: round.reactions.reduce((s, r) => s + r.impact, 0) / round.reactions.length,
|
|
441
|
+
maxVolatility: Math.max(...round.reactions.map(r => Math.abs(r.impact))),
|
|
442
|
+
dynamics: round.emergent_dynamics ?? [],
|
|
443
|
+
interventionCount: 0,
|
|
444
|
+
});
|
|
445
|
+
await new Promise(r => setTimeout(r, 400));
|
|
446
|
+
}
|
|
447
|
+
// Run governed with narrative events
|
|
448
|
+
const governedResult = await runGovernedComparison(request, world, resolved.paths, narrativeEvents);
|
|
449
|
+
const nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, world);
|
|
450
|
+
for (const round of governedResult.governed.swarm.rounds) {
|
|
451
|
+
const reactions = round.reactions.map(r => {
|
|
452
|
+
const verdict = (0, worldBridge_1.evaluateScenarioGuard)({ ...request, scenario: `[R${round.round}] ${r.stakeholder_id}: ${r.reaction}` }, nvWorld, { trace: true, level: "standard" });
|
|
453
|
+
return {
|
|
454
|
+
stakeholder_id: r.stakeholder_id,
|
|
455
|
+
reaction: r.reaction,
|
|
456
|
+
impact: r.impact,
|
|
457
|
+
confidence: r.confidence,
|
|
458
|
+
trigger: r.trigger,
|
|
459
|
+
verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
const interventionCount = (round.emergent_dynamics ?? []).filter(d => d.includes("intervention") || d.includes("governance")).length;
|
|
463
|
+
broadcast({
|
|
464
|
+
type: "round",
|
|
465
|
+
round: round.round,
|
|
466
|
+
totalRounds: rounds,
|
|
467
|
+
phase: "governed",
|
|
468
|
+
reactions,
|
|
469
|
+
avgImpact: round.reactions.reduce((s, r) => s + r.impact, 0) / round.reactions.length,
|
|
470
|
+
maxVolatility: Math.max(...round.reactions.map(r => Math.abs(r.impact))),
|
|
471
|
+
dynamics: round.emergent_dynamics ?? [],
|
|
472
|
+
interventionCount,
|
|
473
|
+
});
|
|
474
|
+
await new Promise(r => setTimeout(r, 500));
|
|
475
|
+
}
|
|
476
|
+
broadcast({ type: "complete", result: governedResult });
|
|
477
|
+
}
|
|
478
|
+
const server = http.createServer(async (req, res) => {
|
|
479
|
+
// CORS preflight
|
|
480
|
+
if (req.method === "OPTIONS") {
|
|
481
|
+
res.writeHead(204, {
|
|
482
|
+
"Access-Control-Allow-Origin": "*",
|
|
483
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
484
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
485
|
+
});
|
|
486
|
+
res.end();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (req.url === "/events") {
|
|
490
|
+
res.writeHead(200, {
|
|
491
|
+
"Content-Type": "text/event-stream",
|
|
492
|
+
"Cache-Control": "no-cache",
|
|
493
|
+
"Connection": "keep-alive",
|
|
494
|
+
"Access-Control-Allow-Origin": "*",
|
|
495
|
+
});
|
|
496
|
+
clients.add(res);
|
|
497
|
+
req.on("close", () => clients.delete(res));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (req.url === "/api/worlds" && req.method === "GET") {
|
|
501
|
+
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
502
|
+
const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis"];
|
|
503
|
+
const worlds = worldIds.map(id => {
|
|
504
|
+
try {
|
|
505
|
+
const r = resolveWorld(id);
|
|
506
|
+
return {
|
|
507
|
+
id,
|
|
508
|
+
title: r.title,
|
|
509
|
+
thesis: r.world.thesis,
|
|
510
|
+
stateVariables: r.world.state_variables,
|
|
511
|
+
invariants: r.world.invariants,
|
|
512
|
+
gates: r.world.gates ?? [],
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}).filter(Boolean);
|
|
519
|
+
jsonResponse(res, 200, { worlds });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (req.url === "/api/scenarios" && req.method === "GET") {
|
|
523
|
+
jsonResponse(res, 200, { scenarios: scenarioLibrary_1.SCENARIO_LIBRARY });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (req.url === "/api/narratives" && req.method === "GET") {
|
|
527
|
+
jsonResponse(res, 200, { narratives: narrativeInjection_1.NARRATIVE_PRESETS });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (req.url === "/api/variants" && req.method === "GET") {
|
|
531
|
+
const variants = loadAllVariants();
|
|
532
|
+
jsonResponse(res, 200, { variants });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (req.url === "/api/save-variant" && req.method === "POST") {
|
|
536
|
+
try {
|
|
537
|
+
const body = await readBody(req);
|
|
538
|
+
const payload = JSON.parse(body);
|
|
539
|
+
if (!payload.name || !payload.baseWorld) {
|
|
540
|
+
jsonResponse(res, 400, { error: "name and baseWorld are required" });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const id = slugify(payload.name);
|
|
544
|
+
const variant = {
|
|
545
|
+
id,
|
|
546
|
+
name: payload.name,
|
|
547
|
+
description: payload.description ?? "",
|
|
548
|
+
baseWorld: payload.baseWorld,
|
|
549
|
+
stateOverrides: payload.stateOverrides ?? {},
|
|
550
|
+
events: payload.events ?? [],
|
|
551
|
+
rounds: payload.rounds ?? 5,
|
|
552
|
+
createdAt: new Date().toISOString(),
|
|
553
|
+
lastResult: payload.lastResult,
|
|
554
|
+
};
|
|
555
|
+
const filepath = saveVariant(variant);
|
|
556
|
+
jsonResponse(res, 200, { status: "saved", variant, filepath });
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
560
|
+
}
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (req.url?.startsWith("/api/delete-variant/") && req.method === "DELETE") {
|
|
564
|
+
const variantId = req.url.split("/api/delete-variant/")[1];
|
|
565
|
+
if (variantId && deleteVariant(variantId)) {
|
|
566
|
+
jsonResponse(res, 200, { status: "deleted", id: variantId });
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
jsonResponse(res, 404, { error: "Variant not found" });
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (req.url === "/api/run-sim" && req.method === "POST") {
|
|
574
|
+
if (isRunning) {
|
|
575
|
+
jsonResponse(res, 409, { error: "Simulation already running" });
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const body = await readBody(req);
|
|
580
|
+
const config = JSON.parse(body);
|
|
581
|
+
isRunning = true;
|
|
582
|
+
jsonResponse(res, 200, { status: "started", config });
|
|
583
|
+
// Run simulation async, streaming via SSE
|
|
584
|
+
runSimulation(config).catch(err => {
|
|
585
|
+
broadcast({ type: "complete", result: { error: String(err) } });
|
|
586
|
+
}).finally(() => { isRunning = false; });
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// ── Governance Evaluate Endpoint ──
|
|
594
|
+
// Universal bridge endpoint: external simulators POST actions here for governance evaluation.
|
|
595
|
+
// Contract: { actor, action, payload, state? } → { decision: ALLOW|BLOCK|MODIFY, reason, modified_action? }
|
|
596
|
+
if (req.url === "/api/evaluate" && req.method === "POST") {
|
|
597
|
+
try {
|
|
598
|
+
const body = await readBody(req);
|
|
599
|
+
const payload = JSON.parse(body);
|
|
600
|
+
if (!payload.actor || !payload.action) {
|
|
601
|
+
jsonResponse(res, 400, { error: "actor and action are required" });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
// Resolve world for evaluation
|
|
605
|
+
const worldId = payload.world ?? "trading";
|
|
606
|
+
let nvWorld;
|
|
607
|
+
try {
|
|
608
|
+
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
609
|
+
const resolved = resolveWorld(worldId);
|
|
610
|
+
const request = {
|
|
611
|
+
scenario: resolved.scenario,
|
|
612
|
+
stakeholders: resolved.stakeholders,
|
|
613
|
+
assumptions: resolved.assumptions,
|
|
614
|
+
constraints: resolved.constraints,
|
|
615
|
+
depth: resolved.depth,
|
|
616
|
+
swarm: resolved.swarm,
|
|
617
|
+
};
|
|
618
|
+
nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, resolved.world);
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
// Fallback: build minimal world
|
|
622
|
+
nvWorld = (0, worldBridge_1.buildWorldFromScenario)({
|
|
623
|
+
scenario: `${payload.actor}: ${payload.action}`,
|
|
624
|
+
stakeholders: [],
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// Enable action_space — bridge actions are simulation execution, not thinking-only.
|
|
628
|
+
// Without this, the safety layer flags normal actions like "buy" as execution intents
|
|
629
|
+
// in a thinking-only environment.
|
|
630
|
+
nvWorld.world.players = { ...nvWorld.world.players, action_space: true };
|
|
631
|
+
// Inject simulation-specific guards into the world.
|
|
632
|
+
// The guard engine pattern-matches event.intent against guard.intent_patterns.
|
|
633
|
+
// Without guards, everything defaults to ALLOW. These guards define what
|
|
634
|
+
// simulation actions should be governed.
|
|
635
|
+
if (!nvWorld.guards) {
|
|
636
|
+
// Detect world type from request — social media worlds get social guards,
|
|
637
|
+
// financial worlds get trading guards, unknown gets both.
|
|
638
|
+
const isSocial = payload.world === "social-media" || payload.world === "social"
|
|
639
|
+
|| ["create_post", "like_post", "repost", "follow", "unfollow", "create_comment",
|
|
640
|
+
"search_posts", "mute", "unmute", "trend"].includes(payload.action);
|
|
641
|
+
const financialGuards = [
|
|
642
|
+
{
|
|
643
|
+
id: "sim-panic-actions",
|
|
644
|
+
label: "Block panic-driven actions",
|
|
645
|
+
description: "Prevents panic selling, aggressive shorting, and other destabilizing actions during high volatility",
|
|
646
|
+
category: "structural",
|
|
647
|
+
enforcement: "block",
|
|
648
|
+
immutable: true,
|
|
649
|
+
invariant_ref: nvWorld.invariants[0]?.id,
|
|
650
|
+
intent_patterns: ["panic_sell", "panic sell", "panic buy", "panic_buy"],
|
|
651
|
+
default_enabled: true,
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
id: "sim-excessive-leverage",
|
|
655
|
+
label: "Block excessive leverage",
|
|
656
|
+
description: "Prevents increasing leverage positions that could amplify cascades",
|
|
657
|
+
category: "structural",
|
|
658
|
+
enforcement: "block",
|
|
659
|
+
immutable: true,
|
|
660
|
+
intent_patterns: ["increase_leverage", "increase leverage", "max leverage"],
|
|
661
|
+
default_enabled: true,
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
id: "sim-aggressive-actions",
|
|
665
|
+
label: "Pause aggressive market actions",
|
|
666
|
+
description: "Requires review for aggressive buying or shorting that could move markets",
|
|
667
|
+
category: "operational",
|
|
668
|
+
enforcement: "pause",
|
|
669
|
+
immutable: false,
|
|
670
|
+
intent_patterns: ["aggressive_buy", "aggressive buy", "aggressive_short", "short"],
|
|
671
|
+
default_enabled: true,
|
|
672
|
+
},
|
|
673
|
+
];
|
|
674
|
+
// Social media guards for MiroFish/OASIS agent actions.
|
|
675
|
+
// These govern what AI agents can do on simulated social platforms.
|
|
676
|
+
const socialGuards = [
|
|
677
|
+
{
|
|
678
|
+
id: "social-harmful-content",
|
|
679
|
+
label: "Block harmful content creation",
|
|
680
|
+
description: "Prevents agents from posting content that incites panic, spreads disinformation, or promotes harmful behavior",
|
|
681
|
+
category: "structural",
|
|
682
|
+
enforcement: "block",
|
|
683
|
+
immutable: true,
|
|
684
|
+
intent_patterns: ["create_post", "create_comment", "quote_post"],
|
|
685
|
+
content_patterns: ["panic", "crash", "collaps", "sell everything", "market is dead", "scam", "rug pull", "ponzi"],
|
|
686
|
+
default_enabled: true,
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
id: "social-coordinated-manipulation",
|
|
690
|
+
label: "Block coordinated manipulation",
|
|
691
|
+
description: "Prevents agents from engaging in coordinated inauthentic behavior like mass following, mass liking, or brigading",
|
|
692
|
+
category: "structural",
|
|
693
|
+
enforcement: "block",
|
|
694
|
+
immutable: true,
|
|
695
|
+
intent_patterns: ["follow", "like_post", "repost"],
|
|
696
|
+
default_enabled: true,
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: "social-spam-prevention",
|
|
700
|
+
label: "Pause high-frequency posting",
|
|
701
|
+
description: "Rate-limits agents that post too frequently, preventing spam and platform flooding",
|
|
702
|
+
category: "operational",
|
|
703
|
+
enforcement: "pause",
|
|
704
|
+
immutable: false,
|
|
705
|
+
intent_patterns: ["create_post", "create_comment", "repost", "quote_post"],
|
|
706
|
+
default_enabled: true,
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: "social-engagement-farming",
|
|
710
|
+
label: "Block engagement farming",
|
|
711
|
+
description: "Prevents agents from like-bombing, follow-unfollowing, or other engagement manipulation tactics",
|
|
712
|
+
category: "operational",
|
|
713
|
+
enforcement: "block",
|
|
714
|
+
immutable: false,
|
|
715
|
+
intent_patterns: ["like_post", "unlike_post", "follow", "unfollow"],
|
|
716
|
+
default_enabled: true,
|
|
717
|
+
},
|
|
718
|
+
];
|
|
719
|
+
const guards = isSocial ? socialGuards : financialGuards;
|
|
720
|
+
// Build intent vocabulary from selected guards
|
|
721
|
+
const intentVocabulary = {};
|
|
722
|
+
for (const g of guards) {
|
|
723
|
+
for (const pat of g.intent_patterns) {
|
|
724
|
+
if (!intentVocabulary[pat]) {
|
|
725
|
+
intentVocabulary[pat] = { label: pat.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()), pattern: pat };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
nvWorld.guards = { guards, intent_vocabulary: intentVocabulary };
|
|
730
|
+
}
|
|
731
|
+
// Inject custom guards from plain-English rule editor
|
|
732
|
+
if (customGuards.length > 0 && nvWorld.guards) {
|
|
733
|
+
const existingGuards = nvWorld.guards.guards;
|
|
734
|
+
for (const cg of customGuards) {
|
|
735
|
+
existingGuards.push({
|
|
736
|
+
id: cg.id,
|
|
737
|
+
label: cg.label,
|
|
738
|
+
description: cg.description,
|
|
739
|
+
category: cg.category,
|
|
740
|
+
enforcement: cg.enforcement,
|
|
741
|
+
immutable: false,
|
|
742
|
+
intent_patterns: cg.intent_patterns,
|
|
743
|
+
default_enabled: true,
|
|
744
|
+
});
|
|
745
|
+
// Add patterns to vocabulary
|
|
746
|
+
for (const pat of cg.intent_patterns) {
|
|
747
|
+
nvWorld.guards.intent_vocabulary[pat] = { label: pat, pattern: pat };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// Content-aware governance for social media actions.
|
|
752
|
+
// The guard engine matches intent against intent_patterns, but for social media
|
|
753
|
+
// we also need to check message content for harmful patterns.
|
|
754
|
+
const messageContent = typeof payload.payload?.message === "string"
|
|
755
|
+
? payload.payload.message.toLowerCase()
|
|
756
|
+
: typeof payload.payload?.content === "string"
|
|
757
|
+
? payload.payload.content.toLowerCase()
|
|
758
|
+
: "";
|
|
759
|
+
const contentPatterns = ["panic", "crash", "collaps", "sell everything", "market is dead",
|
|
760
|
+
"scam", "rug pull", "ponzi", "buy now or lose", "guaranteed returns", "get rich"];
|
|
761
|
+
const hasHarmfulContent = messageContent.length > 0
|
|
762
|
+
&& contentPatterns.some(p => messageContent.includes(p));
|
|
763
|
+
// If content is harmful and action is content-creating, short-circuit to BLOCK
|
|
764
|
+
const isContentAction = ["create_post", "create_comment", "quote_post", "repost"].includes(payload.action);
|
|
765
|
+
if (hasHarmfulContent && isContentAction) {
|
|
766
|
+
const matchedPattern = contentPatterns.find(p => messageContent.includes(p)) ?? "harmful content";
|
|
767
|
+
broadcast({
|
|
768
|
+
type: "round",
|
|
769
|
+
round: 0,
|
|
770
|
+
totalRounds: 0,
|
|
771
|
+
phase: "governed",
|
|
772
|
+
reactions: [{
|
|
773
|
+
stakeholder_id: payload.actor,
|
|
774
|
+
reaction: payload.action,
|
|
775
|
+
impact: 0,
|
|
776
|
+
confidence: 0.9,
|
|
777
|
+
trigger: "bridge",
|
|
778
|
+
verdict: { status: "BLOCK", reason: `Content violates governance: "${matchedPattern}" detected`, ruleId: "social-harmful-content" },
|
|
779
|
+
}],
|
|
780
|
+
avgImpact: 0,
|
|
781
|
+
maxVolatility: 0,
|
|
782
|
+
dynamics: [],
|
|
783
|
+
interventionCount: 1,
|
|
784
|
+
});
|
|
785
|
+
// Record in session
|
|
786
|
+
currentSession.evaluations.push({
|
|
787
|
+
actor: payload.actor, action: payload.action, decision: "BLOCK",
|
|
788
|
+
reason: `Content blocked: "${matchedPattern}" detected`,
|
|
789
|
+
ruleId: "social-harmful-content", world: payload.world ?? currentSession.world,
|
|
790
|
+
timestamp: Date.now(), payload: payload.payload,
|
|
791
|
+
});
|
|
792
|
+
jsonResponse(res, 200, {
|
|
793
|
+
decision: "BLOCK",
|
|
794
|
+
reason: `Content blocked: "${matchedPattern}" detected in ${payload.action} — violates social media governance policy`,
|
|
795
|
+
rule_id: "social-harmful-content",
|
|
796
|
+
evidence: { matched_pattern: matchedPattern, action: payload.action, actor: payload.actor },
|
|
797
|
+
modified_action: null,
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
// Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
|
|
802
|
+
// Omit `direction` — setting it enables execution-intent safety checks (prompt injection
|
|
803
|
+
// detection) which falsely flag financial terms like "buy" and "sell". Bridge actions are
|
|
804
|
+
// simulation commands, not user prompts.
|
|
805
|
+
const guardEvent = {
|
|
806
|
+
intent: payload.action,
|
|
807
|
+
tool: "simulation",
|
|
808
|
+
scope: `bridge/${payload.actor}`,
|
|
809
|
+
actionCategory: "execute",
|
|
810
|
+
riskLevel: (["panic_sell", "panic_buy", "increase_leverage", "create_post", "repost", "quote_post"].includes(payload.action) ? "high" : "medium"),
|
|
811
|
+
args: {
|
|
812
|
+
actor: payload.actor,
|
|
813
|
+
action: payload.action,
|
|
814
|
+
...(payload.payload ?? {}),
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
// Evaluate directly via the governance module
|
|
818
|
+
let verdict;
|
|
819
|
+
try {
|
|
820
|
+
const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
|
|
821
|
+
verdict = nv.evaluateGuard(guardEvent, nvWorld, {
|
|
822
|
+
trace: true,
|
|
823
|
+
level: "standard",
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
// Fallback to scenario guard evaluation
|
|
828
|
+
verdict = (0, worldBridge_1.evaluateScenarioGuard)({
|
|
829
|
+
scenario: `[BRIDGE] ${payload.actor}: ${payload.action}`,
|
|
830
|
+
stakeholders: [{ id: payload.actor, description: payload.actor, disposition: "neutral", priorities: [] }],
|
|
831
|
+
}, nvWorld, { trace: true, level: "standard" });
|
|
832
|
+
}
|
|
833
|
+
// Map verdict to bridge protocol
|
|
834
|
+
const decision = verdict.status === "BLOCK" ? "BLOCK"
|
|
835
|
+
: verdict.status === "PAUSE" ? "MODIFY"
|
|
836
|
+
: "ALLOW";
|
|
837
|
+
// Broadcast governance event to connected SSE clients
|
|
838
|
+
broadcast({
|
|
839
|
+
type: "round",
|
|
840
|
+
round: 0,
|
|
841
|
+
totalRounds: 0,
|
|
842
|
+
phase: "governed",
|
|
843
|
+
reactions: [{
|
|
844
|
+
stakeholder_id: payload.actor,
|
|
845
|
+
reaction: payload.action,
|
|
846
|
+
impact: 0,
|
|
847
|
+
confidence: 0.5,
|
|
848
|
+
trigger: "bridge",
|
|
849
|
+
verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
|
|
850
|
+
}],
|
|
851
|
+
avgImpact: 0,
|
|
852
|
+
maxVolatility: 0,
|
|
853
|
+
dynamics: [],
|
|
854
|
+
interventionCount: decision !== "ALLOW" ? 1 : 0,
|
|
855
|
+
});
|
|
856
|
+
// Record in session
|
|
857
|
+
currentSession.evaluations.push({
|
|
858
|
+
actor: payload.actor, action: payload.action,
|
|
859
|
+
decision: decision,
|
|
860
|
+
reason: verdict.reason ?? "", ruleId: verdict.ruleId ?? null,
|
|
861
|
+
world: payload.world ?? currentSession.world,
|
|
862
|
+
timestamp: Date.now(), payload: payload.payload,
|
|
863
|
+
});
|
|
864
|
+
currentSession.guardCount = customGuards.length + (nvWorld.guards?.guards?.length ?? 0);
|
|
865
|
+
// Persist to audit trail on disk
|
|
866
|
+
auditTrail.logVerdict({
|
|
867
|
+
agent: payload.actor,
|
|
868
|
+
action: payload.action,
|
|
869
|
+
actionType: payload.payload?.type ?? "unknown",
|
|
870
|
+
verdict: decision,
|
|
871
|
+
reason: verdict.reason ?? "",
|
|
872
|
+
confidence: verdict.confidence ?? 0.5,
|
|
873
|
+
rulesFired: verdict.ruleId ? [{
|
|
874
|
+
id: verdict.ruleId,
|
|
875
|
+
description: verdict.reason ?? "",
|
|
876
|
+
effect: decision === "BLOCK" ? "blocked" : decision === "MODIFY" ? "dampened" : "monitored",
|
|
877
|
+
impactReduction: decision === "BLOCK" ? 1 : decision === "MODIFY" ? 0.5 : 0,
|
|
878
|
+
}] : [],
|
|
879
|
+
worldState: payload.world ?? currentSession.world,
|
|
880
|
+
});
|
|
881
|
+
jsonResponse(res, 200, {
|
|
882
|
+
decision,
|
|
883
|
+
reason: verdict.reason ?? null,
|
|
884
|
+
rule_id: verdict.ruleId ?? null,
|
|
885
|
+
evidence: verdict.evidence ?? null,
|
|
886
|
+
modified_action: decision === "MODIFY" ? payload.payload : null,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
catch (err) {
|
|
890
|
+
// Record fail-open in session (payload may be out of scope if JSON parse failed)
|
|
891
|
+
currentSession.evaluations.push({
|
|
892
|
+
actor: "unknown", action: "unknown",
|
|
893
|
+
decision: "ALLOW", reason: "Governance evaluation error — fail open",
|
|
894
|
+
ruleId: null, world: currentSession.world, timestamp: Date.now(),
|
|
895
|
+
});
|
|
896
|
+
// Fail open — return ALLOW on any error
|
|
897
|
+
jsonResponse(res, 200, {
|
|
898
|
+
decision: "ALLOW",
|
|
899
|
+
reason: "Governance evaluation error — fail open",
|
|
900
|
+
rule_id: null,
|
|
901
|
+
evidence: null,
|
|
902
|
+
modified_action: null,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
// ── Plain-English Rule Parser ──
|
|
908
|
+
// Parses natural language rules into guard definitions (local, no LLM needed)
|
|
909
|
+
if (req.url === "/api/parse-rules" && req.method === "POST") {
|
|
910
|
+
try {
|
|
911
|
+
const body = await readBody(req);
|
|
912
|
+
const payload = JSON.parse(body);
|
|
913
|
+
if (!payload.text) {
|
|
914
|
+
jsonResponse(res, 400, { error: "text is required" });
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const lines = payload.text.split("\n").map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith("#"));
|
|
918
|
+
const rules = lines.map((line, i) => parseNaturalLanguageRule(line, i));
|
|
919
|
+
jsonResponse(res, 200, { rules: rules.filter(Boolean), raw: lines });
|
|
920
|
+
}
|
|
921
|
+
catch (err) {
|
|
922
|
+
jsonResponse(res, 400, { error: "Invalid request" });
|
|
923
|
+
}
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
// Apply parsed rules to the active governance context
|
|
927
|
+
if (req.url === "/api/apply-rules" && req.method === "POST") {
|
|
928
|
+
try {
|
|
929
|
+
const body = await readBody(req);
|
|
930
|
+
const payload = JSON.parse(body);
|
|
931
|
+
if (!payload.rules || !Array.isArray(payload.rules)) {
|
|
932
|
+
jsonResponse(res, 400, { error: "rules array is required" });
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
// Store custom rules in memory for the session
|
|
936
|
+
customGuards.length = 0;
|
|
937
|
+
for (const rule of payload.rules) {
|
|
938
|
+
customGuards.push({
|
|
939
|
+
id: rule.id,
|
|
940
|
+
label: rule.description,
|
|
941
|
+
description: rule.description,
|
|
942
|
+
category: "custom",
|
|
943
|
+
enforcement: rule.enforcement,
|
|
944
|
+
immutable: false,
|
|
945
|
+
intent_patterns: rule.intent_patterns,
|
|
946
|
+
default_enabled: true,
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
jsonResponse(res, 200, {
|
|
950
|
+
status: "applied",
|
|
951
|
+
applied: customGuards.length,
|
|
952
|
+
enforcement: "full",
|
|
953
|
+
detail: "Rules enforced across guard engine (intent blocking) AND invariant engine (system dynamics)",
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
catch (err) {
|
|
957
|
+
jsonResponse(res, 400, { error: "Invalid request" });
|
|
958
|
+
}
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
// ── Session Reporting Endpoints ──
|
|
962
|
+
// Connect the serve runtime to the enforce reporting pipeline.
|
|
963
|
+
// Users can request reports, stats, and recommendations from live governance data.
|
|
964
|
+
// GET /api/session — current session stats (lightweight)
|
|
965
|
+
if (req.url === "/api/session" && req.method === "GET") {
|
|
966
|
+
const evals = currentSession.evaluations;
|
|
967
|
+
const blocked = evals.filter(e => e.decision === "BLOCK").length;
|
|
968
|
+
const modified = evals.filter(e => e.decision === "MODIFY").length;
|
|
969
|
+
const allowed = evals.filter(e => e.decision === "ALLOW").length;
|
|
970
|
+
const uniqueActors = [...new Set(evals.map(e => e.actor))];
|
|
971
|
+
const uniqueActions = [...new Set(evals.map(e => e.action))];
|
|
972
|
+
const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
|
|
973
|
+
jsonResponse(res, 200, {
|
|
974
|
+
sessionId: currentSession.id,
|
|
975
|
+
startedAt: currentSession.startedAt,
|
|
976
|
+
world: currentSession.world,
|
|
977
|
+
guardCount: currentSession.guardCount,
|
|
978
|
+
evaluations: {
|
|
979
|
+
total: evals.length,
|
|
980
|
+
blocked,
|
|
981
|
+
modified,
|
|
982
|
+
allowed,
|
|
983
|
+
},
|
|
984
|
+
agents: uniqueActors,
|
|
985
|
+
actionTypes: uniqueActions,
|
|
986
|
+
triggeredRules,
|
|
987
|
+
historyCount: sessionHistory.length,
|
|
988
|
+
});
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
// GET /api/session/report — full enforcement report (text)
|
|
992
|
+
if (req.url === "/api/session/report" && req.method === "GET") {
|
|
993
|
+
const report = synthesizeSessionReport();
|
|
994
|
+
// Format as human-readable text using the same style as enforce CLI
|
|
995
|
+
const lines = [];
|
|
996
|
+
lines.push("");
|
|
997
|
+
lines.push(" LIVE GOVERNANCE — ENFORCEMENT REPORT");
|
|
998
|
+
lines.push(" " + "=".repeat(70));
|
|
999
|
+
lines.push(` Session: ${report.sessionId}`);
|
|
1000
|
+
lines.push(` Scenario: ${report.scenario}`);
|
|
1001
|
+
lines.push(` Sessions: ${report.runs.length}`);
|
|
1002
|
+
lines.push(` Generated: ${report.generatedAt}`);
|
|
1003
|
+
lines.push("");
|
|
1004
|
+
if (report.runs.length > 0) {
|
|
1005
|
+
lines.push(" RUN HISTORY");
|
|
1006
|
+
lines.push(" " + "-".repeat(70));
|
|
1007
|
+
lines.push(` ${"#".padEnd(4)} ${"World".padEnd(25)} ${"Rules".padEnd(8)} ${"Evals".padEnd(8)} ${"Blocked".padEnd(10)} ${"Effectiveness"}`);
|
|
1008
|
+
lines.push(" " + "-".repeat(70));
|
|
1009
|
+
for (const run of report.runs) {
|
|
1010
|
+
const name = run.worldName.length > 23 ? run.worldName.slice(0, 22) + "…" : run.worldName;
|
|
1011
|
+
lines.push(` ${String(run.iteration).padEnd(4)} ${name.padEnd(25)} ${String(run.ruleCount).padEnd(8)} ${String(run.governanceStats.totalEvaluations).padEnd(8)} ${String(run.governanceStats.verdicts.block).padEnd(10)} ${(run.comparison.governanceEffectiveness * 100).toFixed(0)}%`);
|
|
1012
|
+
}
|
|
1013
|
+
lines.push("");
|
|
1014
|
+
lines.push(" DIVERGENCE ANALYSIS");
|
|
1015
|
+
lines.push(" " + "-".repeat(70));
|
|
1016
|
+
lines.push(` Stability trend: ${report.divergence.stabilityTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1017
|
+
lines.push(` Collapse trend: ${report.divergence.collapseTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1018
|
+
lines.push(` Effectiveness trend: ${report.divergence.effectivenessTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1019
|
+
lines.push(` Total divergence: ${report.divergence.totalDivergence}`);
|
|
1020
|
+
lines.push("");
|
|
1021
|
+
lines.push(` ${report.divergence.narrative}`);
|
|
1022
|
+
// Blocked action breakdown
|
|
1023
|
+
const allEvals = [
|
|
1024
|
+
...sessionHistory.flatMap(s => s.evaluations),
|
|
1025
|
+
...currentSession.evaluations,
|
|
1026
|
+
];
|
|
1027
|
+
const blockedEvals = allEvals.filter(e => e.decision === "BLOCK");
|
|
1028
|
+
if (blockedEvals.length > 0) {
|
|
1029
|
+
lines.push("");
|
|
1030
|
+
lines.push(" BLOCKED ACTIONS");
|
|
1031
|
+
lines.push(" " + "-".repeat(70));
|
|
1032
|
+
// Group by rule
|
|
1033
|
+
const byRule = {};
|
|
1034
|
+
for (const e of blockedEvals) {
|
|
1035
|
+
const key = e.ruleId ?? "unknown";
|
|
1036
|
+
if (!byRule[key])
|
|
1037
|
+
byRule[key] = { count: 0, actors: new Set(), actions: new Set() };
|
|
1038
|
+
byRule[key].count++;
|
|
1039
|
+
byRule[key].actors.add(e.actor);
|
|
1040
|
+
byRule[key].actions.add(e.action);
|
|
1041
|
+
}
|
|
1042
|
+
const sorted = Object.entries(byRule).sort((a, b) => b[1].count - a[1].count);
|
|
1043
|
+
for (const [ruleId, data] of sorted) {
|
|
1044
|
+
lines.push(` ${ruleId}: ${data.count} blocks across ${data.actors.size} agent(s)`);
|
|
1045
|
+
lines.push(` Actions: ${[...data.actions].join(", ")}`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// Recommendations (deterministic, no LLM needed)
|
|
1049
|
+
lines.push("");
|
|
1050
|
+
lines.push(" RECOMMENDATIONS");
|
|
1051
|
+
lines.push(" " + "-".repeat(70));
|
|
1052
|
+
const best = report.runs[report.divergence.bestIteration - 1];
|
|
1053
|
+
if (best) {
|
|
1054
|
+
const totalEvals = best.governanceStats.totalEvaluations;
|
|
1055
|
+
const blockRate = totalEvals > 0 ? best.governanceStats.verdicts.block / totalEvals : 0;
|
|
1056
|
+
if (blockRate > 0.5) {
|
|
1057
|
+
lines.push(" HIGH BLOCK RATE — Rules may be too aggressive.");
|
|
1058
|
+
lines.push(" Try relaxing constraints or adding MODIFY actions instead of hard blocks.");
|
|
1059
|
+
lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
|
|
1060
|
+
}
|
|
1061
|
+
else if (blockRate < 0.05 && totalEvals > 20) {
|
|
1062
|
+
lines.push(" LOW BLOCK RATE — Rules may be too permissive.");
|
|
1063
|
+
lines.push(" Try adding content-specific guards or lowering thresholds.");
|
|
1064
|
+
lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
|
|
1065
|
+
}
|
|
1066
|
+
else if (blockRate > 0) {
|
|
1067
|
+
lines.push(` Governance is active: ${(blockRate * 100).toFixed(0)}% of actions blocked.`);
|
|
1068
|
+
lines.push(" To iterate: apply different rules via POST /api/apply-rules, then POST /api/session/reset.");
|
|
1069
|
+
lines.push(" This creates a new session for side-by-side comparison.");
|
|
1070
|
+
}
|
|
1071
|
+
if (report.runs.length === 1) {
|
|
1072
|
+
lines.push("");
|
|
1073
|
+
lines.push(" EXPERIMENT: Apply different rules and reset to compare sessions.");
|
|
1074
|
+
lines.push(" This will show divergence: how different governance changes outcomes.");
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
lines.push("");
|
|
1078
|
+
lines.push(" RECOMMENDATION");
|
|
1079
|
+
lines.push(" " + "-".repeat(70));
|
|
1080
|
+
lines.push(` ${report.recommendation}`);
|
|
1081
|
+
}
|
|
1082
|
+
lines.push("");
|
|
1083
|
+
lines.push(" " + "=".repeat(70));
|
|
1084
|
+
lines.push(" NeuroVerse Policy Enforcement System — Live Governance");
|
|
1085
|
+
lines.push(" Design rules. Run reality. See what changes.");
|
|
1086
|
+
lines.push(" " + "=".repeat(70));
|
|
1087
|
+
lines.push("");
|
|
1088
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
1089
|
+
res.end(lines.join("\n"));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
// GET /api/session/report.json — full enforcement report (JSON)
|
|
1093
|
+
if (req.url === "/api/session/report.json" && req.method === "GET") {
|
|
1094
|
+
const report = synthesizeSessionReport();
|
|
1095
|
+
jsonResponse(res, 200, report);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
// GET /api/session/evaluations — raw evaluation log
|
|
1099
|
+
if (req.url === "/api/session/evaluations" && req.method === "GET") {
|
|
1100
|
+
jsonResponse(res, 200, {
|
|
1101
|
+
sessionId: currentSession.id,
|
|
1102
|
+
count: currentSession.evaluations.length,
|
|
1103
|
+
evaluations: currentSession.evaluations.slice(-200), // last 200
|
|
1104
|
+
});
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
// POST /api/session/reset — snapshot current session and start fresh
|
|
1108
|
+
// This is how users create multi-run comparisons:
|
|
1109
|
+
// 1. Apply rules A → run simulation → POST /api/session/reset
|
|
1110
|
+
// 2. Apply rules B → run simulation → GET /api/session/report
|
|
1111
|
+
// Now the report shows divergence between rules A and rules B.
|
|
1112
|
+
if (req.url === "/api/session/reset" && req.method === "POST") {
|
|
1113
|
+
if (currentSession.evaluations.length > 0) {
|
|
1114
|
+
sessionHistory.push({ ...currentSession, evaluations: [...currentSession.evaluations] });
|
|
1115
|
+
}
|
|
1116
|
+
const body = await readBody(req).catch(() => "{}");
|
|
1117
|
+
const opts = JSON.parse(body || "{}");
|
|
1118
|
+
currentSession = {
|
|
1119
|
+
id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1120
|
+
startedAt: new Date().toISOString(),
|
|
1121
|
+
world: opts.world ?? currentSession.world,
|
|
1122
|
+
guardCount: customGuards.length,
|
|
1123
|
+
evaluations: [],
|
|
1124
|
+
};
|
|
1125
|
+
jsonResponse(res, 200, {
|
|
1126
|
+
status: "reset",
|
|
1127
|
+
newSessionId: currentSession.id,
|
|
1128
|
+
previousSessions: sessionHistory.length,
|
|
1129
|
+
message: `Session reset. ${sessionHistory.length} session(s) in history. Apply new rules and evaluate to compare.`,
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
// POST /api/session/save — save session as experiment with lineage
|
|
1134
|
+
if (req.url === "/api/session/save" && req.method === "POST") {
|
|
1135
|
+
const report = synthesizeSessionReport();
|
|
1136
|
+
if (report.runs.length === 0) {
|
|
1137
|
+
jsonResponse(res, 400, { error: "No evaluations to save. Send actions to /api/evaluate first." });
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const experiment = {
|
|
1141
|
+
id: `exp-live-${Date.now().toString(36)}`,
|
|
1142
|
+
savedAt: new Date().toISOString(),
|
|
1143
|
+
scenario: report.scenario,
|
|
1144
|
+
source: "live-governance",
|
|
1145
|
+
sessions: report.runs.length,
|
|
1146
|
+
totalEvaluations: report.runs.reduce((sum, r) => sum + r.governanceStats.totalEvaluations, 0),
|
|
1147
|
+
metrics: {
|
|
1148
|
+
stability: report.runs[report.divergence.bestIteration - 1]?.metrics.stabilityScore ?? 0,
|
|
1149
|
+
effectiveness: report.runs[report.divergence.bestIteration - 1]?.comparison.governanceEffectiveness ?? 0,
|
|
1150
|
+
collapseProbability: report.runs[report.divergence.bestIteration - 1]?.metrics.collapseProbability ?? 0,
|
|
1151
|
+
},
|
|
1152
|
+
divergence: report.divergence,
|
|
1153
|
+
recommendation: report.recommendation,
|
|
1154
|
+
report,
|
|
1155
|
+
};
|
|
1156
|
+
// Save to experiments/ directory
|
|
1157
|
+
try {
|
|
1158
|
+
if (!fs.existsSync("experiments"))
|
|
1159
|
+
fs.mkdirSync("experiments", { recursive: true });
|
|
1160
|
+
const filePath = path.join("experiments", `${experiment.id}.json`);
|
|
1161
|
+
fs.writeFileSync(filePath, JSON.stringify(experiment, null, 2));
|
|
1162
|
+
jsonResponse(res, 200, {
|
|
1163
|
+
status: "saved",
|
|
1164
|
+
experimentId: experiment.id,
|
|
1165
|
+
filePath,
|
|
1166
|
+
metrics: experiment.metrics,
|
|
1167
|
+
message: `Saved to ${filePath}. View with: npx nv-sim enforce --load ${filePath}`,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
catch (err) {
|
|
1171
|
+
jsonResponse(res, 500, { error: "Failed to save experiment", detail: String(err) });
|
|
1172
|
+
}
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
// List available live adapters
|
|
1176
|
+
if (req.url === "/api/adapters" && req.method === "GET") {
|
|
1177
|
+
const adapters = Object.values(liveAdapter_1.ADAPTER_REGISTRY).map(a => ({
|
|
1178
|
+
id: a.id,
|
|
1179
|
+
label: a.label,
|
|
1180
|
+
description: a.description,
|
|
1181
|
+
}));
|
|
1182
|
+
jsonResponse(res, 200, { adapters });
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
// Run simulation via live adapter (external process)
|
|
1186
|
+
if (req.url === "/api/run-live" && req.method === "POST") {
|
|
1187
|
+
if (isRunning) {
|
|
1188
|
+
jsonResponse(res, 409, { error: "Simulation already running" });
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
try {
|
|
1192
|
+
const body = await readBody(req);
|
|
1193
|
+
const payload = JSON.parse(body);
|
|
1194
|
+
const adapter = (0, liveAdapter_1.createAdapter)(payload.adapterId, payload.options);
|
|
1195
|
+
if (!adapter) {
|
|
1196
|
+
jsonResponse(res, 400, { error: `Unknown adapter: ${payload.adapterId}` });
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
isRunning = true;
|
|
1200
|
+
jsonResponse(res, 200, { status: "started", adapter: payload.adapterId });
|
|
1201
|
+
// Resolve world for governance evaluation
|
|
1202
|
+
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
1203
|
+
const resolved = resolveWorld(payload.worldId ?? "trading");
|
|
1204
|
+
const world = { ...resolved.world };
|
|
1205
|
+
if (payload.stateOverrides) {
|
|
1206
|
+
const updatedVars = world.state_variables.map(sv => {
|
|
1207
|
+
if (payload.stateOverrides && sv.id in payload.stateOverrides) {
|
|
1208
|
+
return { ...sv, default_value: payload.stateOverrides[sv.id] };
|
|
1209
|
+
}
|
|
1210
|
+
return sv;
|
|
1211
|
+
});
|
|
1212
|
+
world.state_variables = updatedVars;
|
|
1213
|
+
}
|
|
1214
|
+
const request = {
|
|
1215
|
+
scenario: resolved.scenario,
|
|
1216
|
+
stakeholders: resolved.stakeholders,
|
|
1217
|
+
assumptions: resolved.assumptions,
|
|
1218
|
+
constraints: resolved.constraints,
|
|
1219
|
+
depth: resolved.depth,
|
|
1220
|
+
swarm: resolved.swarm,
|
|
1221
|
+
};
|
|
1222
|
+
const nvWorld = (0, worldBridge_1.buildWorldFromScenario)(request, world);
|
|
1223
|
+
// Send meta event with adapter source
|
|
1224
|
+
broadcast({
|
|
1225
|
+
type: "meta",
|
|
1226
|
+
source: payload.adapterId,
|
|
1227
|
+
scenario: resolved.scenario,
|
|
1228
|
+
worldThesis: world.thesis,
|
|
1229
|
+
agents: [], // will be populated dynamically from adapter
|
|
1230
|
+
invariants: world.invariants.map(inv => ({ id: inv.id, description: inv.description })),
|
|
1231
|
+
gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
|
|
1232
|
+
narrativeEvents: [],
|
|
1233
|
+
totalRounds: 0,
|
|
1234
|
+
});
|
|
1235
|
+
// Listen for rounds from the adapter
|
|
1236
|
+
adapter.on("round", (liveRound) => {
|
|
1237
|
+
// Build lookup of original verdicts from adapter's adaptation data.
|
|
1238
|
+
// When an external bridge (e.g. MiroFish) already applied governance,
|
|
1239
|
+
// the verdict lives in adaptation.deltas — use it instead of re-evaluating
|
|
1240
|
+
// (re-evaluating the MODIFIED action would return ALLOW, hiding the BLOCK).
|
|
1241
|
+
const adaptationByAgent = new Map();
|
|
1242
|
+
if (liveRound.adaptation?.deltas) {
|
|
1243
|
+
for (const delta of liveRound.adaptation.deltas) {
|
|
1244
|
+
adaptationByAgent.set(delta.agent, {
|
|
1245
|
+
status: delta.decision,
|
|
1246
|
+
reason: delta.reason,
|
|
1247
|
+
ruleId: delta.rule,
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const reactions = liveRound.agentActions.map(a => {
|
|
1252
|
+
// Priority: per-action verdict > adaptation delta > local re-evaluation
|
|
1253
|
+
const actionVerdict = a.verdict ? { status: a.verdict.status, reason: a.verdict.reason, ruleId: a.verdict.rule } : null;
|
|
1254
|
+
const bridgeVerdict = actionVerdict ?? adaptationByAgent.get(a.agent);
|
|
1255
|
+
let verdict;
|
|
1256
|
+
if (bridgeVerdict) {
|
|
1257
|
+
verdict = bridgeVerdict;
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
const guard = (0, worldBridge_1.evaluateScenarioGuard)({ ...request, scenario: `[R${liveRound.round}] ${a.agent}: ${a.action}` }, nvWorld, { trace: true, level: "standard" });
|
|
1261
|
+
verdict = { status: guard.status, reason: guard.reason, ruleId: guard.ruleId };
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
stakeholder_id: a.agent,
|
|
1265
|
+
reaction: a.action,
|
|
1266
|
+
impact: a.impact,
|
|
1267
|
+
confidence: a.confidence ?? 0.5,
|
|
1268
|
+
trigger: liveRound.source,
|
|
1269
|
+
verdict,
|
|
1270
|
+
};
|
|
1271
|
+
});
|
|
1272
|
+
const interventionCount = reactions.filter(r => r.verdict.status !== "ALLOW").length;
|
|
1273
|
+
broadcast({
|
|
1274
|
+
type: "round",
|
|
1275
|
+
round: liveRound.round,
|
|
1276
|
+
totalRounds: 0, // unknown for live streams
|
|
1277
|
+
phase: "governed",
|
|
1278
|
+
reactions,
|
|
1279
|
+
avgImpact: reactions.reduce((s, r) => s + r.impact, 0) / (reactions.length || 1),
|
|
1280
|
+
maxVolatility: Math.max(0, ...reactions.map(r => Math.abs(r.impact))),
|
|
1281
|
+
dynamics: liveRound.emergentDynamics ?? [],
|
|
1282
|
+
interventionCount,
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
adapter.on("complete", () => {
|
|
1286
|
+
isRunning = false;
|
|
1287
|
+
});
|
|
1288
|
+
adapter.on("error", (err) => {
|
|
1289
|
+
broadcast({ type: "complete", result: { error: String(err) } });
|
|
1290
|
+
isRunning = false;
|
|
1291
|
+
});
|
|
1292
|
+
adapter.start().catch(err => {
|
|
1293
|
+
broadcast({ type: "complete", result: { error: String(err) } });
|
|
1294
|
+
isRunning = false;
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
catch (err) {
|
|
1298
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1299
|
+
}
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
// ── AUDIT TRAIL ENDPOINTS ──
|
|
1303
|
+
// GET /api/audit — summary of current session's audit trail
|
|
1304
|
+
if (req.url === "/api/audit" && req.method === "GET") {
|
|
1305
|
+
jsonResponse(res, 200, auditTrail.summary());
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
// GET /api/audit/entries — all entries from current session (with optional filters)
|
|
1309
|
+
if (req.url?.startsWith("/api/audit/entries") && req.method === "GET") {
|
|
1310
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1311
|
+
const entries = auditTrail.query({
|
|
1312
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1313
|
+
agent: url.searchParams.get("agent") ?? undefined,
|
|
1314
|
+
verdict: url.searchParams.get("verdict") ?? undefined,
|
|
1315
|
+
runId: url.searchParams.get("runId") ?? undefined,
|
|
1316
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
1317
|
+
before: url.searchParams.get("before") ?? undefined,
|
|
1318
|
+
});
|
|
1319
|
+
jsonResponse(res, 200, {
|
|
1320
|
+
sessionId: auditTrail.getSessionId(),
|
|
1321
|
+
count: entries.length,
|
|
1322
|
+
entries,
|
|
1323
|
+
});
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
// GET /api/audit/entries/text — human-readable audit trail
|
|
1327
|
+
if (req.url?.startsWith("/api/audit/entries/text") && req.method === "GET") {
|
|
1328
|
+
const entries = auditTrail.readAll();
|
|
1329
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
1330
|
+
res.end((0, auditTrace_1.formatAuditTrail)(entries, { verbose: true }));
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
// GET /api/audit/sessions — list all audit sessions on disk
|
|
1334
|
+
if (req.url === "/api/audit/sessions" && req.method === "GET") {
|
|
1335
|
+
const sessions = (0, auditTrace_1.listAuditSessions)();
|
|
1336
|
+
jsonResponse(res, 200, {
|
|
1337
|
+
sessions: sessions.map((id) => {
|
|
1338
|
+
const trail = (0, auditTrace_1.loadAuditSession)(id);
|
|
1339
|
+
return trail.summary();
|
|
1340
|
+
}),
|
|
1341
|
+
});
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
// GET /api/audit/sessions/:id — load a specific session's audit trail
|
|
1345
|
+
const auditSessionMatch = req.url?.match(/^\/api\/audit\/sessions\/([^/]+)$/);
|
|
1346
|
+
if (auditSessionMatch && req.method === "GET") {
|
|
1347
|
+
const sessionId = decodeURIComponent(auditSessionMatch[1]);
|
|
1348
|
+
const trail = (0, auditTrace_1.loadAuditSession)(sessionId);
|
|
1349
|
+
const entries = trail.readAll();
|
|
1350
|
+
if (entries.length === 0) {
|
|
1351
|
+
jsonResponse(res, 404, { error: `Audit session "${sessionId}" not found or empty` });
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
jsonResponse(res, 200, {
|
|
1355
|
+
...trail.summary(),
|
|
1356
|
+
entries,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
// GET /api/audit/search — search across ALL sessions
|
|
1362
|
+
if (req.url?.startsWith("/api/audit/search") && req.method === "GET") {
|
|
1363
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1364
|
+
const results = (0, auditTrace_1.searchAuditTrails)({
|
|
1365
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1366
|
+
agent: url.searchParams.get("agent") ?? undefined,
|
|
1367
|
+
verdict: url.searchParams.get("verdict") ?? undefined,
|
|
1368
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
1369
|
+
before: url.searchParams.get("before") ?? undefined,
|
|
1370
|
+
});
|
|
1371
|
+
jsonResponse(res, 200, {
|
|
1372
|
+
count: results.length,
|
|
1373
|
+
entries: results,
|
|
1374
|
+
});
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
// Serve the interactive dashboard
|
|
1378
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1379
|
+
res.end(INTERACTIVE_DASHBOARD_HTML);
|
|
1380
|
+
});
|
|
1381
|
+
server.listen(port, () => {
|
|
1382
|
+
onReady(`http://localhost:${port}`);
|
|
1383
|
+
});
|
|
1384
|
+
return { server };
|
|
1385
|
+
}
|
|
1386
|
+
const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
1387
|
+
<html lang="en">
|
|
1388
|
+
<head>
|
|
1389
|
+
<meta charset="UTF-8">
|
|
1390
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1391
|
+
<title>NV-SIM — Scenario Control Platform</title>
|
|
1392
|
+
<style>
|
|
1393
|
+
/* ── Theme Variables ── */
|
|
1394
|
+
:root {
|
|
1395
|
+
--bg-primary: #0f0f0f;
|
|
1396
|
+
--bg-secondary: #141414;
|
|
1397
|
+
--bg-surface: #1a1a1a;
|
|
1398
|
+
--bg-elevated: #222;
|
|
1399
|
+
--border: #2a2a2a;
|
|
1400
|
+
--border-subtle: #333;
|
|
1401
|
+
--text-primary: #f0f0f0;
|
|
1402
|
+
--text-secondary: #b0b0b0;
|
|
1403
|
+
--text-muted: #888;
|
|
1404
|
+
--text-faint: #666;
|
|
1405
|
+
--accent: #818cf8;
|
|
1406
|
+
--accent-bg: #1e1e3a;
|
|
1407
|
+
--green: #4ade80;
|
|
1408
|
+
--green-bg: #0a2a14;
|
|
1409
|
+
--red: #f87171;
|
|
1410
|
+
--red-bg: #2d0a0a;
|
|
1411
|
+
--yellow: #fbbf24;
|
|
1412
|
+
--yellow-bg: #2d2006;
|
|
1413
|
+
--blue: #60a5fa;
|
|
1414
|
+
--blue-bg: #1e293b;
|
|
1415
|
+
--purple: #a78bfa;
|
|
1416
|
+
}
|
|
1417
|
+
body.light {
|
|
1418
|
+
--bg-primary: #f5f5f5;
|
|
1419
|
+
--bg-secondary: #eaeaea;
|
|
1420
|
+
--bg-surface: #e0e0e0;
|
|
1421
|
+
--bg-elevated: #d4d4d4;
|
|
1422
|
+
--border: #c0c0c0;
|
|
1423
|
+
--border-subtle: #b0b0b0;
|
|
1424
|
+
--text-primary: #1a1a1a;
|
|
1425
|
+
--text-secondary: #444;
|
|
1426
|
+
--text-muted: #666;
|
|
1427
|
+
--text-faint: #888;
|
|
1428
|
+
--accent: #6366f1;
|
|
1429
|
+
--accent-bg: #e8e8ff;
|
|
1430
|
+
--green: #16a34a;
|
|
1431
|
+
--green-bg: #dcfce7;
|
|
1432
|
+
--red: #dc2626;
|
|
1433
|
+
--red-bg: #fee2e2;
|
|
1434
|
+
--yellow: #ca8a04;
|
|
1435
|
+
--yellow-bg: #fef9c3;
|
|
1436
|
+
--blue: #2563eb;
|
|
1437
|
+
--blue-bg: #dbeafe;
|
|
1438
|
+
--purple: #7c3aed;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1442
|
+
body { font-family: 'SF Mono', 'Fira Code', monospace; background: var(--bg-primary); color: var(--text-primary); overflow: hidden; transition: background 0.3s, color 0.3s; }
|
|
1443
|
+
.header { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
1444
|
+
.header h1 { font-size: 15px; color: var(--text-primary); }
|
|
1445
|
+
.header .sub { font-size: 11px; color: var(--text-muted); margin-left: 12px; }
|
|
1446
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
1447
|
+
.theme-toggle { background: var(--bg-surface); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: 6px; font-family: inherit; font-size: 11px; cursor: pointer; transition: all 0.2s; }
|
|
1448
|
+
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
|
1449
|
+
.status { font-size: 11px; padding: 3px 10px; border-radius: 10px; }
|
|
1450
|
+
.status.idle { background: var(--accent-bg); color: var(--accent); }
|
|
1451
|
+
.status.live { background: var(--green-bg); color: var(--green); animation: pulse 2s infinite; }
|
|
1452
|
+
.status.complete { background: var(--accent-bg); color: var(--accent); }
|
|
1453
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
1454
|
+
|
|
1455
|
+
.layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }
|
|
1456
|
+
|
|
1457
|
+
/* LEFT PANEL — Controls */
|
|
1458
|
+
.controls { background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px; }
|
|
1459
|
+
.ctrl-section { margin-bottom: 20px; }
|
|
1460
|
+
.ctrl-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
|
|
1461
|
+
|
|
1462
|
+
.ctrl-row { margin-bottom: 12px; }
|
|
1463
|
+
.ctrl-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; display: flex; justify-content: space-between; }
|
|
1464
|
+
.ctrl-label .val { color: var(--text-primary); font-weight: 600; }
|
|
1465
|
+
|
|
1466
|
+
input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: var(--bg-elevated); border-radius: 2px; outline: none; }
|
|
1467
|
+
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; }
|
|
1468
|
+
input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: none; }
|
|
1469
|
+
|
|
1470
|
+
select { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
|
|
1471
|
+
|
|
1472
|
+
.toggle-row { display: flex; align-items: center; gap: 8px; }
|
|
1473
|
+
.toggle { position: relative; width: 36px; height: 20px; background: var(--border-subtle); border-radius: 10px; cursor: pointer; transition: background 0.2s; }
|
|
1474
|
+
.toggle.on { background: var(--green); }
|
|
1475
|
+
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
|
|
1476
|
+
.toggle.on::after { transform: translateX(16px); }
|
|
1477
|
+
.toggle-label { font-size: 12px; color: var(--text-secondary); }
|
|
1478
|
+
|
|
1479
|
+
.inject-row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
1480
|
+
.inject-row select { flex: 1; }
|
|
1481
|
+
.inject-row input { width: 50px; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 4px 6px; border-radius: 4px; font-family: inherit; font-size: 12px; text-align: center; }
|
|
1482
|
+
.inject-list { margin-bottom: 8px; }
|
|
1483
|
+
.inject-item { font-size: 11px; color: var(--text-secondary); padding: 3px 6px; background: var(--bg-surface); border-radius: 3px; margin-bottom: 3px; display: flex; justify-content: space-between; }
|
|
1484
|
+
.inject-item .remove { color: var(--red); cursor: pointer; }
|
|
1485
|
+
|
|
1486
|
+
.btn { width: 100%; padding: 10px; border: none; border-radius: 6px; font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s; }
|
|
1487
|
+
.btn-run { background: var(--green); color: #0a0a0a; }
|
|
1488
|
+
.btn-run:hover { background: #22c55e; }
|
|
1489
|
+
.btn-run:disabled { background: var(--bg-elevated); color: var(--text-faint); cursor: not-allowed; }
|
|
1490
|
+
.btn-add { background: var(--bg-elevated); color: var(--accent); padding: 6px; font-size: 11px; }
|
|
1491
|
+
.btn-add:hover { background: var(--accent-bg); }
|
|
1492
|
+
|
|
1493
|
+
.scenario-btn { display: block; width: 100%; padding: 8px 10px; margin-bottom: 4px; background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; color: var(--text-primary); font-family: inherit; font-size: 11px; text-align: left; cursor: pointer; }
|
|
1494
|
+
.scenario-btn:hover { border-color: var(--accent); background: var(--accent-bg); }
|
|
1495
|
+
.scenario-btn .stitle { font-weight: 600; color: var(--text-primary); }
|
|
1496
|
+
.scenario-btn .sdesc { color: var(--text-muted); font-size: 10px; margin-top: 2px; }
|
|
1497
|
+
|
|
1498
|
+
/* Save variant */
|
|
1499
|
+
.btn-save { background: var(--accent-bg); color: var(--accent); margin-top: 0; }
|
|
1500
|
+
.btn-save:hover { background: var(--accent-bg); filter: brightness(1.2); }
|
|
1501
|
+
.btn-confirm { flex: 1; background: var(--green); color: #0a0a0a; padding: 7px; font-size: 11px; }
|
|
1502
|
+
.btn-cancel { flex: 1; background: var(--bg-elevated); color: var(--text-muted); padding: 7px; font-size: 11px; }
|
|
1503
|
+
.save-input { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 7px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
|
|
1504
|
+
.save-input:focus { border-color: var(--accent); outline: none; }
|
|
1505
|
+
|
|
1506
|
+
/* Variant cards */
|
|
1507
|
+
.variant-card { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; padding: 8px 10px; margin-bottom: 4px; cursor: pointer; position: relative; }
|
|
1508
|
+
.variant-card:hover { border-color: var(--green); }
|
|
1509
|
+
.variant-card .vname { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
1510
|
+
.variant-card .vdesc { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
|
1511
|
+
.variant-card .vmeta { font-size: 10px; color: var(--text-faint); margin-top: 4px; }
|
|
1512
|
+
.variant-card .vmeta .vresult { color: var(--green); }
|
|
1513
|
+
.variant-card .vdelete { position: absolute; top: 6px; right: 8px; color: var(--red); font-size: 10px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
|
|
1514
|
+
.variant-card:hover .vdelete { opacity: 1; }
|
|
1515
|
+
.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
|
+
|
|
1517
|
+
/* 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; }
|
|
1543
|
+
.center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
|
|
1544
|
+
.verdict { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-left: 4px; }
|
|
1545
|
+
.verdict.ALLOW { background: var(--green-bg); color: var(--green); }
|
|
1546
|
+
.verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1547
|
+
.verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
|
|
1548
|
+
|
|
1549
|
+
/* Chart */
|
|
1550
|
+
.chart-container { position: relative; height: 100%; min-height: 150px; }
|
|
1551
|
+
canvas { width: 100% !important; height: 100% !important; }
|
|
1552
|
+
|
|
1553
|
+
/* Simulation Trace */
|
|
1554
|
+
.trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
1555
|
+
.trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
|
|
1556
|
+
.trace-round-header:hover { background: var(--bg-elevated); }
|
|
1557
|
+
.trace-phase { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
1558
|
+
.trace-phase.baseline { background: var(--blue-bg); color: var(--blue); }
|
|
1559
|
+
.trace-phase.governed { background: var(--green-bg); color: var(--green); }
|
|
1560
|
+
.trace-round-label { font-size: 11px; color: var(--text-primary); font-weight: 600; }
|
|
1561
|
+
.trace-round-metrics { margin-left: auto; font-size: 10px; color: var(--text-muted); display: flex; gap: 10px; }
|
|
1562
|
+
.trace-body { padding: 0 10px 8px; }
|
|
1563
|
+
.trace-body[data-collapsed="true"] { display: none; }
|
|
1564
|
+
.trace-section { margin-top: 6px; }
|
|
1565
|
+
.trace-section-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 3px; display: flex; align-items: center; gap: 5px; }
|
|
1566
|
+
.trace-section-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
|
|
1567
|
+
.trace-section-label.event::before { background: #f59e0b; }
|
|
1568
|
+
.trace-section-label.agents::before { background: #3b82f6; }
|
|
1569
|
+
.trace-section-label.governance::before { background: #10b981; }
|
|
1570
|
+
.trace-event-item { font-size: 10px; color: var(--text-primary); padding: 3px 0 3px 11px; border-left: 2px solid #f59e0b; margin-left: 2px; }
|
|
1571
|
+
.trace-event-severity { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; margin-left: 4px; }
|
|
1572
|
+
.trace-event-severity.major, .trace-event-severity.extreme { background: var(--red-bg); color: var(--red); }
|
|
1573
|
+
.trace-event-severity.moderate { background: var(--yellow-bg); color: var(--yellow); }
|
|
1574
|
+
.trace-event-severity.minor { background: var(--green-bg); color: var(--green); }
|
|
1575
|
+
.trace-agent-item { font-size: 10px; padding: 2px 0 2px 11px; border-left: 2px solid #3b82f6; margin-left: 2px; display: flex; align-items: center; gap: 6px; }
|
|
1576
|
+
.trace-agent-name { color: var(--blue); font-weight: 500; min-width: 80px; }
|
|
1577
|
+
.trace-agent-action { color: var(--text-secondary); flex: 1; }
|
|
1578
|
+
.trace-agent-impact { font-size: 9px; font-weight: 600; min-width: 36px; text-align: right; }
|
|
1579
|
+
.trace-agent-impact.positive { color: var(--green); }
|
|
1580
|
+
.trace-agent-impact.negative { color: var(--red); }
|
|
1581
|
+
.trace-gov-item { font-size: 10px; padding: 3px 6px 3px 11px; border-left: 2px solid #10b981; margin-left: 2px; display: flex; align-items: center; gap: 6px; }
|
|
1582
|
+
.trace-gov-rule { font-size: 9px; color: var(--text-muted); font-family: monospace; }
|
|
1583
|
+
.trace-gov-reason { color: var(--text-secondary); flex: 1; }
|
|
1584
|
+
.trace-dynamics { font-size: 10px; color: var(--purple); padding: 2px 0 2px 11px; border-left: 2px solid #7c3aed; margin-left: 2px; font-style: italic; }
|
|
1585
|
+
.trace-arrow { color: var(--text-faint); font-size: 10px; text-align: center; padding: 2px 0; }
|
|
1586
|
+
.trace-empty { font-size: 10px; color: var(--text-faint); font-style: italic; padding: 4px 0; }
|
|
1587
|
+
|
|
1588
|
+
/* World info */
|
|
1589
|
+
.world-thesis { font-size: 11px; color: var(--text-secondary); font-style: italic; margin-bottom: 8px; }
|
|
1590
|
+
.rule-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; transition: border-color 0.2s; }
|
|
1591
|
+
.rule-card:hover { border-color: var(--text-muted); }
|
|
1592
|
+
.rule-card.type-invariant { border-left: 4px solid var(--green); }
|
|
1593
|
+
.rule-card.type-gate { border-left: 4px solid var(--red); }
|
|
1594
|
+
.rule-card.type-warning { border-left: 4px solid var(--yellow); }
|
|
1595
|
+
.rule-card.type-modify { border-left: 4px solid #60a5fa; }
|
|
1596
|
+
.rule-card .rule-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
1597
|
+
.rule-card .rule-icon { font-size: 12px; flex-shrink: 0; }
|
|
1598
|
+
.rule-card .rule-title { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
1599
|
+
.rule-card .rule-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
|
|
1600
|
+
.rule-card .rule-meta { font-size: 10px; color: var(--text-muted); margin-top: 6px; opacity: 0.8; }
|
|
1601
|
+
.rule-card .rule-why { font-size: 10px; color: var(--text-muted); margin-top: 5px; font-style: italic; padding-top: 5px; border-top: 1px solid var(--border); }
|
|
1602
|
+
.rule-card .rule-impact { display: none; font-size: 10px; margin-top: 6px; padding: 5px 8px; border-radius: 4px; background: var(--bg-surface, var(--bg-secondary)); }
|
|
1603
|
+
.rule-card .rule-impact.visible { display: block; }
|
|
1604
|
+
.rule-card .rule-impact .impact-stat { color: var(--text-primary); font-weight: 600; }
|
|
1605
|
+
.rule-card .rule-impact .impact-label { color: var(--text-muted); }
|
|
1606
|
+
.rule-card.user-rule { border-left-color: #818cf8; background: rgba(129, 140, 248, 0.05); }
|
|
1607
|
+
.rule-source-tag { font-size: 9px; font-weight: 700; color: #818cf8; background: rgba(129, 140, 248, 0.15); padding: 1px 5px; border-radius: 3px; margin-left: auto; letter-spacing: 0.5px; }
|
|
1608
|
+
|
|
1609
|
+
/* Empty state */
|
|
1610
|
+
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-faint); }
|
|
1611
|
+
.empty-state .icon { font-size: 48px; margin-bottom: 12px; }
|
|
1612
|
+
.empty-state .msg { font-size: 13px; color: var(--text-muted); }
|
|
1613
|
+
.empty-state .hint { font-size: 11px; color: var(--text-faint); margin-top: 6px; }
|
|
1614
|
+
|
|
1615
|
+
/* System Shift Card */
|
|
1616
|
+
.system-shift { display: none; margin: 12px 16px; border: 1px solid var(--green-bg); border-radius: 8px; background: var(--bg-secondary); overflow: hidden; animation: fadeInUp 0.4s ease; }
|
|
1617
|
+
.system-shift.visible { display: block; }
|
|
1618
|
+
@keyframes fadeInUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
1619
|
+
.ss-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background: var(--green-bg); border-bottom: 1px solid var(--green-bg); }
|
|
1620
|
+
.ss-icon { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgba(74,222,128,0.4); }
|
|
1621
|
+
.ss-title { font-size: 11px; font-weight: 700; color: var(--green); text-transform: uppercase; letter-spacing: 1.5px; }
|
|
1622
|
+
.ss-rule { font-size: 13px; font-weight: 600; color: var(--text-primary); padding: 10px 14px 0; }
|
|
1623
|
+
.ss-body { padding: 10px 14px 14px; display: grid; gap: 8px; }
|
|
1624
|
+
.ss-section { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
|
|
1625
|
+
.ss-section-label { font-size: 9px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
|
|
1626
|
+
.ss-adapt-rate { font-size: 20px; font-weight: 700; color: var(--green); }
|
|
1627
|
+
.ss-adapt-desc { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
|
|
1628
|
+
.ss-shift-item { font-size: 11px; color: var(--text-secondary); padding: 2px 0; display: flex; align-items: center; gap: 6px; }
|
|
1629
|
+
.ss-shift-arrow { color: var(--green); font-weight: 600; }
|
|
1630
|
+
.ss-pattern-tag { display: inline-block; font-size: 10px; padding: 2px 8px; background: var(--accent-bg); color: var(--accent); border-radius: 3px; margin-right: 4px; margin-bottom: 4px; }
|
|
1631
|
+
.ss-impact-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--text-secondary); padding: 2px 0; }
|
|
1632
|
+
.ss-impact-delta { color: var(--green); font-weight: 600; }
|
|
1633
|
+
.ss-impact-delta.negative { color: var(--red); }
|
|
1634
|
+
.ss-narrative { font-size: 12px; color: var(--text-primary); line-height: 1.5; font-style: italic; border-left: 2px solid var(--green); padding-left: 10px; }
|
|
1635
|
+
.ss-scale { font-size: 10px; color: var(--text-muted); padding: 0 14px 6px; }
|
|
1636
|
+
.ss-scale strong { color: var(--text-secondary); }
|
|
1637
|
+
.ss-flow { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 10px; color: var(--text-faint); }
|
|
1638
|
+
.ss-flow-arrow { color: var(--green); }
|
|
1639
|
+
.ss-raw-toggle { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: var(--bg-secondary); border: none; border-top: 1px solid var(--border); color: var(--text-muted); font-family: inherit; font-size: 10px; cursor: pointer; width: 100%; text-align: left; transition: color 0.2s; }
|
|
1640
|
+
.ss-raw-toggle:hover { color: var(--text-secondary); }
|
|
1641
|
+
.ss-raw-toggle .arrow { transition: transform 0.2s; }
|
|
1642
|
+
.ss-raw-toggle.open .arrow { transform: rotate(90deg); }
|
|
1643
|
+
.ss-raw-detail { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
|
|
1644
|
+
.ss-raw-detail.open { max-height: 200px; overflow-y: auto; }
|
|
1645
|
+
.ss-raw-list { padding: 6px 12px; }
|
|
1646
|
+
.ss-raw-item { font-size: 10px; color: var(--text-muted); padding: 2px 0; display: flex; gap: 6px; }
|
|
1647
|
+
.ss-raw-item .raw-agent { color: var(--text-secondary); min-width: 100px; }
|
|
1648
|
+
.ss-raw-item .raw-action { color: var(--text-secondary); flex: 1; }
|
|
1649
|
+
.ss-raw-item .raw-verdict { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; }
|
|
1650
|
+
.ss-raw-item .raw-verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1651
|
+
.ss-raw-item .raw-verdict.MODIFY { background: var(--yellow-bg); color: var(--yellow); }
|
|
1652
|
+
|
|
1653
|
+
/* Integration Quick-Start (in controls panel) */
|
|
1654
|
+
.integrate-section { background: var(--accent-bg); border: 1px solid var(--accent-bg); border-radius: 6px; padding: 10px 12px; margin-top: 8px; }
|
|
1655
|
+
.integrate-section h4 { font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
|
1656
|
+
.integrate-code { font-size: 10px; color: var(--text-primary); background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; padding: 8px; overflow-x: auto; white-space: pre; line-height: 1.5; }
|
|
1657
|
+
.integrate-code .kw { color: var(--accent); }
|
|
1658
|
+
.integrate-code .str { color: var(--green); }
|
|
1659
|
+
.integrate-code .comment { color: var(--text-muted); }
|
|
1660
|
+
.integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
|
|
1661
|
+
.integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
|
|
1662
|
+
|
|
1663
|
+
/* Rule editor */
|
|
1664
|
+
.rule-editor { margin-top: 8px; }
|
|
1665
|
+
.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; }
|
|
1666
|
+
.rule-input:focus { border-color: var(--accent); outline: none; }
|
|
1667
|
+
.rule-input::placeholder { color: var(--text-faint); }
|
|
1668
|
+
.btn-parse { background: var(--accent-bg); color: var(--accent); margin-top: 6px; padding: 8px; font-size: 11px; }
|
|
1669
|
+
.btn-parse:hover { filter: brightness(1.2); }
|
|
1670
|
+
.btn-parse:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1671
|
+
.parsed-rules { margin-top: 8px; }
|
|
1672
|
+
.parsed-rule { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; transition: border-color 0.2s; }
|
|
1673
|
+
.parsed-rule:hover { border-color: var(--text-muted); }
|
|
1674
|
+
.parsed-rule.enforcement-block { border-left: 4px solid var(--red); }
|
|
1675
|
+
.parsed-rule.enforcement-allow { border-left: 4px solid var(--green); }
|
|
1676
|
+
.parsed-rule.enforcement-modify { border-left: 4px solid #60a5fa; }
|
|
1677
|
+
.parsed-rule.enforcement-warn { border-left: 4px solid var(--yellow); }
|
|
1678
|
+
.parsed-rule .pr-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
1679
|
+
.parsed-rule .pr-icon { font-size: 12px; }
|
|
1680
|
+
.parsed-rule .pr-action { font-weight: 700; font-size: 12px; color: var(--text-primary); text-transform: capitalize; }
|
|
1681
|
+
.parsed-rule .pr-desc { color: var(--text-secondary); font-size: 11px; line-height: 1.4; }
|
|
1682
|
+
.parsed-rule .pr-patterns { color: var(--text-muted); font-size: 10px; margin-top: 6px; opacity: 0.8; }
|
|
1683
|
+
.btn-apply-rules { background: var(--green); color: #0a0a0a; margin-top: 6px; padding: 8px; font-size: 11px; }
|
|
1684
|
+
.btn-apply-rules:hover { filter: brightness(0.9); }
|
|
1685
|
+
.rule-status { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
|
|
1686
|
+
.rule-status.success { color: var(--green); }
|
|
1687
|
+
.rule-status.error { color: var(--red); }
|
|
1688
|
+
.rule-examples { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.6; }
|
|
1689
|
+
.rule-examples code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
|
|
1690
|
+
</style>
|
|
1691
|
+
</head>
|
|
1692
|
+
<body>
|
|
1693
|
+
<div class="header">
|
|
1694
|
+
<div style="display:flex;align-items:center">
|
|
1695
|
+
<h1>NV-SIM</h1>
|
|
1696
|
+
<span class="sub">Scenario Control Platform</span>
|
|
1697
|
+
</div>
|
|
1698
|
+
<div class="header-right">
|
|
1699
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">Light Mode</button>
|
|
1700
|
+
<span id="status" class="status idle">Ready</span>
|
|
1701
|
+
</div>
|
|
1702
|
+
</div>
|
|
1703
|
+
|
|
1704
|
+
<div class="layout">
|
|
1705
|
+
<!-- LEFT: CONTROLS -->
|
|
1706
|
+
<div class="controls" id="controls-panel">
|
|
1707
|
+
<!-- Simulation Engine selector -->
|
|
1708
|
+
<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>
|
|
1714
|
+
</div>
|
|
1715
|
+
<div id="engine-status" style="font-size:10px;color:#555;margin-top:4px"></div>
|
|
1716
|
+
</div>
|
|
1717
|
+
|
|
1718
|
+
<!-- World selector -->
|
|
1719
|
+
<div class="ctrl-section">
|
|
1720
|
+
<h3>World</h3>
|
|
1721
|
+
<div class="ctrl-row">
|
|
1722
|
+
<select id="world-select"></select>
|
|
1723
|
+
</div>
|
|
1724
|
+
<div id="world-thesis" class="world-thesis"></div>
|
|
1725
|
+
</div>
|
|
1726
|
+
|
|
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>
|
|
1731
|
+
</div>
|
|
1732
|
+
|
|
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: Block panic selling during high volatility Limit leverage to 5x maximum 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>
|
|
1748
|
+
</div>
|
|
1749
|
+
</div>
|
|
1750
|
+
</div>
|
|
1751
|
+
|
|
1752
|
+
<!-- Scenario presets -->
|
|
1753
|
+
<div class="ctrl-section">
|
|
1754
|
+
<h3>Scenario Presets</h3>
|
|
1755
|
+
<div id="scenario-list"></div>
|
|
1756
|
+
</div>
|
|
1757
|
+
|
|
1758
|
+
<!-- Narrative injection -->
|
|
1759
|
+
<div class="ctrl-section">
|
|
1760
|
+
<h3>Narrative Events</h3>
|
|
1761
|
+
<div class="inject-row">
|
|
1762
|
+
<select id="event-select"></select>
|
|
1763
|
+
<input type="number" id="event-round" min="1" max="20" value="3" placeholder="R">
|
|
1764
|
+
</div>
|
|
1765
|
+
<button class="btn btn-add" id="add-event-btn">+ Add Event</button>
|
|
1766
|
+
<div id="inject-list" class="inject-list" style="margin-top:8px"></div>
|
|
1767
|
+
</div>
|
|
1768
|
+
|
|
1769
|
+
<!-- Rounds -->
|
|
1770
|
+
<div class="ctrl-section">
|
|
1771
|
+
<h3>Simulation</h3>
|
|
1772
|
+
<div class="ctrl-row">
|
|
1773
|
+
<div class="ctrl-label">
|
|
1774
|
+
<span>Rounds</span>
|
|
1775
|
+
<span class="val" id="rounds-val">5</span>
|
|
1776
|
+
</div>
|
|
1777
|
+
<input type="range" id="rounds-slider" min="3" max="12" value="5">
|
|
1778
|
+
</div>
|
|
1779
|
+
</div>
|
|
1780
|
+
|
|
1781
|
+
<!-- Run button -->
|
|
1782
|
+
<button class="btn btn-run" id="run-btn">Run Simulation</button>
|
|
1783
|
+
|
|
1784
|
+
<!-- Save as variant -->
|
|
1785
|
+
<div id="save-section" style="margin-top:12px">
|
|
1786
|
+
<button class="btn btn-save" id="save-btn">Save as World Variant</button>
|
|
1787
|
+
<div id="save-form" style="display:none;margin-top:8px">
|
|
1788
|
+
<input type="text" id="variant-name" placeholder="Variant name (e.g. Hormuz Closed + 3x Leverage)" class="save-input">
|
|
1789
|
+
<input type="text" id="variant-desc" placeholder="What does this variant test?" class="save-input" style="margin-top:4px">
|
|
1790
|
+
<div style="display:flex;gap:6px;margin-top:6px">
|
|
1791
|
+
<button class="btn btn-confirm" id="confirm-save-btn">Save</button>
|
|
1792
|
+
<button class="btn btn-cancel" id="cancel-save-btn">Cancel</button>
|
|
1793
|
+
</div>
|
|
1794
|
+
</div>
|
|
1795
|
+
</div>
|
|
1796
|
+
|
|
1797
|
+
<!-- Saved variants -->
|
|
1798
|
+
<div class="ctrl-section" style="margin-top:16px">
|
|
1799
|
+
<h3>Saved Variants</h3>
|
|
1800
|
+
<div id="variant-list"><div style="font-size:11px;color:#333">No saved variants yet</div></div>
|
|
1801
|
+
</div>
|
|
1802
|
+
|
|
1803
|
+
<!-- Integration Quick-Start -->
|
|
1804
|
+
<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
|
|
1809
|
+
|
|
1810
|
+
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>
|
|
1814
|
+
)
|
|
1815
|
+
|
|
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>
|
|
1820
|
+
</div>
|
|
1821
|
+
<div style="font-size:10px;color:#444;margin-top:6px">
|
|
1822
|
+
Fail-open · 500ms timeout · Stateless
|
|
1823
|
+
</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
|
|
1827
|
+
<span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
|
|
1828
|
+
</div>
|
|
1829
|
+
</div>
|
|
1830
|
+
|
|
1831
|
+
<!-- Session Report Panel -->
|
|
1832
|
+
<div class="ctrl-section" id="session-panel">
|
|
1833
|
+
<h3 class="ctrl-label">SESSION</h3>
|
|
1834
|
+
<div class="metric-grid" style="grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
|
|
1835
|
+
<div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
|
|
1836
|
+
<div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
|
|
1837
|
+
<div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
|
|
1838
|
+
<div class="metric-box"><div class="value" id="s-allowed" style="color:#4ade80">0</div><div class="label">Allowed</div></div>
|
|
1839
|
+
</div>
|
|
1840
|
+
<div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
|
|
1841
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
1842
|
+
<button onclick="viewSessionReport()" style="flex:1;padding:6px 8px;background:#1e1b4b;color:#818cf8;border:1px solid #312e81;border-radius:4px;cursor:pointer;font-size:11px">View Report</button>
|
|
1843
|
+
<button onclick="resetSession()" style="flex:1;padding:6px 8px;background:#1c1917;color:#a8a29e;border:1px solid #292524;border-radius:4px;cursor:pointer;font-size:11px">Reset & Compare</button>
|
|
1844
|
+
<button onclick="saveExperiment()" style="flex:1;padding:6px 8px;background:#052e16;color:#4ade80;border:1px solid #14532d;border-radius:4px;cursor:pointer;font-size:11px">Save</button>
|
|
1845
|
+
</div>
|
|
1846
|
+
<div id="session-history" style="margin-top:6px;font-size:10px;color:#666"></div>
|
|
1847
|
+
</div>
|
|
1848
|
+
</div>
|
|
1849
|
+
</div>
|
|
1850
|
+
|
|
1851
|
+
<!-- RIGHT: VIEWER -->
|
|
1852
|
+
<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>
|
|
1862
|
+
</div>
|
|
1863
|
+
<div class="vpanel">
|
|
1864
|
+
<h2>World Rules Active</h2>
|
|
1865
|
+
<div id="active-invariants"></div>
|
|
1866
|
+
</div>
|
|
1867
|
+
</div>
|
|
1868
|
+
|
|
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">>_</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>
|
|
1875
|
+
</div>
|
|
1876
|
+
<div class="vpanel">
|
|
1877
|
+
<h2>Impact Timeline</h2>
|
|
1878
|
+
<div class="chart-container"><canvas id="chart"></canvas></div>
|
|
1879
|
+
</div>
|
|
1880
|
+
</div>
|
|
1881
|
+
|
|
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>
|
|
1921
|
+
</div>
|
|
1922
|
+
</div>
|
|
1923
|
+
|
|
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>
|
|
1926
|
+
<div id="log"></div>
|
|
1927
|
+
</div>
|
|
1928
|
+
</div>
|
|
1929
|
+
</div>
|
|
1930
|
+
|
|
1931
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"><\/script>
|
|
1932
|
+
<script>
|
|
1933
|
+
// ============================================
|
|
1934
|
+
// STATE
|
|
1935
|
+
// ============================================
|
|
1936
|
+
let worlds = [];
|
|
1937
|
+
let scenarios = {};
|
|
1938
|
+
let narratives = {};
|
|
1939
|
+
let currentWorld = null;
|
|
1940
|
+
let injectedEvents = [];
|
|
1941
|
+
let totalInterventions = 0;
|
|
1942
|
+
let baselineImpacts = [];
|
|
1943
|
+
let governedImpacts = [];
|
|
1944
|
+
let chartLabels = [];
|
|
1945
|
+
let chart = null;
|
|
1946
|
+
let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
|
|
1947
|
+
let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
|
|
1948
|
+
|
|
1949
|
+
const statusEl = document.getElementById('status');
|
|
1950
|
+
const worldSelect = document.getElementById('world-select');
|
|
1951
|
+
const stateVarsSection = document.getElementById('state-vars-section');
|
|
1952
|
+
const stateVarsEl = document.getElementById('state-vars');
|
|
1953
|
+
const scenarioListEl = document.getElementById('scenario-list');
|
|
1954
|
+
const eventSelect = document.getElementById('event-select');
|
|
1955
|
+
const eventRoundInput = document.getElementById('event-round');
|
|
1956
|
+
const injectListEl = document.getElementById('inject-list');
|
|
1957
|
+
const roundsSlider = document.getElementById('rounds-slider');
|
|
1958
|
+
const roundsVal = document.getElementById('rounds-val');
|
|
1959
|
+
const runBtn = document.getElementById('run-btn');
|
|
1960
|
+
const agentsEl = document.getElementById('agents');
|
|
1961
|
+
const logEl = document.getElementById('log');
|
|
1962
|
+
const activeInvEl = document.getElementById('active-invariants');
|
|
1963
|
+
const engineSelect = document.getElementById('engine-select');
|
|
1964
|
+
const engineStatusEl = document.getElementById('engine-status');
|
|
1965
|
+
const traceSourceEl = document.getElementById('trace-source');
|
|
1966
|
+
|
|
1967
|
+
// ============================================
|
|
1968
|
+
// INIT — Load worlds, scenarios, narratives, adapters
|
|
1969
|
+
// ============================================
|
|
1970
|
+
async function init() {
|
|
1971
|
+
const [wRes, sRes, nRes, aRes] = await Promise.all([
|
|
1972
|
+
fetch('/api/worlds').then(r => r.json()),
|
|
1973
|
+
fetch('/api/scenarios').then(r => r.json()),
|
|
1974
|
+
fetch('/api/narratives').then(r => r.json()),
|
|
1975
|
+
fetch('/api/adapters').then(r => r.json()).catch(() => ({ adapters: [] })),
|
|
1976
|
+
]);
|
|
1977
|
+
|
|
1978
|
+
worlds = wRes.worlds;
|
|
1979
|
+
scenarios = sRes.scenarios;
|
|
1980
|
+
narratives = nRes.narratives;
|
|
1981
|
+
|
|
1982
|
+
// Populate engine selector with live adapters
|
|
1983
|
+
(aRes.adapters || []).forEach(function(a) {
|
|
1984
|
+
const opt = document.createElement('option');
|
|
1985
|
+
opt.value = a.id;
|
|
1986
|
+
opt.textContent = a.label;
|
|
1987
|
+
engineSelect.appendChild(opt);
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Populate world select
|
|
1991
|
+
worlds.forEach(w => {
|
|
1992
|
+
const opt = document.createElement('option');
|
|
1993
|
+
opt.value = w.id;
|
|
1994
|
+
opt.textContent = w.title;
|
|
1995
|
+
worldSelect.appendChild(opt);
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
// Populate event select
|
|
1999
|
+
Object.entries(narratives).forEach(([id, ev]) => {
|
|
2000
|
+
const opt = document.createElement('option');
|
|
2001
|
+
opt.value = id;
|
|
2002
|
+
opt.textContent = ev.headline.slice(0, 40);
|
|
2003
|
+
eventSelect.appendChild(opt);
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
// Populate scenario presets
|
|
2007
|
+
Object.entries(scenarios).forEach(([id, s]) => {
|
|
2008
|
+
const btn = document.createElement('button');
|
|
2009
|
+
btn.className = 'scenario-btn';
|
|
2010
|
+
btn.innerHTML = '<div class="stitle">' + s.title + '</div><div class="sdesc">' + s.description + '</div>';
|
|
2011
|
+
btn.onclick = () => loadScenario(id, s);
|
|
2012
|
+
scenarioListEl.appendChild(btn);
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
// Select first world
|
|
2016
|
+
if (worlds.length > 0) selectWorld(worlds[0].id);
|
|
2017
|
+
|
|
2018
|
+
// Load saved variants
|
|
2019
|
+
await loadVariants();
|
|
2020
|
+
|
|
2021
|
+
// Connect SSE
|
|
2022
|
+
connectSSE();
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function selectWorld(worldId) {
|
|
2026
|
+
currentWorld = worlds.find(w => w.id === worldId);
|
|
2027
|
+
if (!currentWorld) return;
|
|
2028
|
+
|
|
2029
|
+
worldSelect.value = worldId;
|
|
2030
|
+
document.getElementById('world-thesis').textContent = '"' + currentWorld.thesis + '"';
|
|
2031
|
+
|
|
2032
|
+
// Render state variable controls
|
|
2033
|
+
if (currentWorld.stateVariables && currentWorld.stateVariables.length > 0) {
|
|
2034
|
+
stateVarsSection.style.display = '';
|
|
2035
|
+
stateVarsEl.innerHTML = '';
|
|
2036
|
+
currentWorld.stateVariables.forEach(sv => {
|
|
2037
|
+
const row = document.createElement('div');
|
|
2038
|
+
row.className = 'ctrl-row';
|
|
2039
|
+
|
|
2040
|
+
if (sv.type === 'number' && sv.range) {
|
|
2041
|
+
const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
|
|
2042
|
+
row.innerHTML =
|
|
2043
|
+
'<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
|
|
2044
|
+
'<input type="range" id="sv-' + sv.id + '" min="' + sv.range.min + '" max="' + sv.range.max + '" step="' + step + '" value="' + sv.default_value + '" data-sv="' + sv.id + '">';
|
|
2045
|
+
stateVarsEl.appendChild(row);
|
|
2046
|
+
const slider = row.querySelector('input');
|
|
2047
|
+
slider.addEventListener('input', () => {
|
|
2048
|
+
document.getElementById('sv-val-' + sv.id).textContent = slider.value;
|
|
2049
|
+
});
|
|
2050
|
+
} else if (sv.type === 'enum' && sv.enum_values) {
|
|
2051
|
+
row.innerHTML =
|
|
2052
|
+
'<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
|
|
2053
|
+
'<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
|
|
2054
|
+
sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
|
|
2055
|
+
'</select>';
|
|
2056
|
+
stateVarsEl.appendChild(row);
|
|
2057
|
+
} else if (sv.type === 'boolean') {
|
|
2058
|
+
row.innerHTML =
|
|
2059
|
+
'<div class="toggle-row">' +
|
|
2060
|
+
'<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
|
|
2061
|
+
'<span class="toggle-label">' + sv.label + '</span>' +
|
|
2062
|
+
'</div>';
|
|
2063
|
+
stateVarsEl.appendChild(row);
|
|
2064
|
+
const toggle = row.querySelector('.toggle');
|
|
2065
|
+
toggle.addEventListener('click', () => {
|
|
2066
|
+
toggle.classList.toggle('on');
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
} else {
|
|
2071
|
+
stateVarsSection.style.display = 'none';
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Show invariants
|
|
2075
|
+
activeInvEl.innerHTML = currentWorld.invariants.map(inv =>
|
|
2076
|
+
'<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
|
|
2077
|
+
).join('') + (currentWorld.gates || []).map(g =>
|
|
2078
|
+
'<div class="inv-item" style="color:' + (g.severity === 'critical' ? '#f87171' : '#fbbf24') + '">[' + g.id + '] ' + g.label + '</div>'
|
|
2079
|
+
).join('');
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function loadScenario(id, scenario) {
|
|
2083
|
+
// Set world
|
|
2084
|
+
selectWorld(scenario.world);
|
|
2085
|
+
// Set events
|
|
2086
|
+
injectedEvents = scenario.events.slice();
|
|
2087
|
+
renderInjectedEvents();
|
|
2088
|
+
// Set rounds
|
|
2089
|
+
const r = scenario.rounds || 5;
|
|
2090
|
+
roundsSlider.value = Math.min(r, 12);
|
|
2091
|
+
roundsVal.textContent = Math.min(r, 12);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// ============================================
|
|
2095
|
+
// NARRATIVE EVENT INJECTION
|
|
2096
|
+
// ============================================
|
|
2097
|
+
document.getElementById('add-event-btn').addEventListener('click', () => {
|
|
2098
|
+
const eventId = eventSelect.value;
|
|
2099
|
+
const round = parseInt(eventRoundInput.value);
|
|
2100
|
+
if (!eventId || isNaN(round)) return;
|
|
2101
|
+
injectedEvents.push(eventId + '@' + round);
|
|
2102
|
+
renderInjectedEvents();
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
function renderInjectedEvents() {
|
|
2106
|
+
injectListEl.innerHTML = injectedEvents.map((ev, i) =>
|
|
2107
|
+
'<div class="inject-item"><span>' + ev + '</span><span class="remove" data-idx="' + i + '">x</span></div>'
|
|
2108
|
+
).join('');
|
|
2109
|
+
injectListEl.querySelectorAll('.remove').forEach(el => {
|
|
2110
|
+
el.addEventListener('click', () => {
|
|
2111
|
+
injectedEvents.splice(parseInt(el.dataset.idx), 1);
|
|
2112
|
+
renderInjectedEvents();
|
|
2113
|
+
});
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// ============================================
|
|
2118
|
+
// WORLD SELECT
|
|
2119
|
+
// ============================================
|
|
2120
|
+
worldSelect.addEventListener('change', () => selectWorld(worldSelect.value));
|
|
2121
|
+
roundsSlider.addEventListener('input', () => { roundsVal.textContent = roundsSlider.value; });
|
|
2122
|
+
|
|
2123
|
+
// ============================================
|
|
2124
|
+
// RUN SIMULATION
|
|
2125
|
+
// ============================================
|
|
2126
|
+
runBtn.addEventListener('click', async () => {
|
|
2127
|
+
if (!currentWorld) return;
|
|
2128
|
+
runBtn.disabled = true;
|
|
2129
|
+
runBtn.textContent = 'Running...';
|
|
2130
|
+
|
|
2131
|
+
// Reset viewer state
|
|
2132
|
+
totalInterventions = 0;
|
|
2133
|
+
baselineImpacts = [];
|
|
2134
|
+
governedImpacts = [];
|
|
2135
|
+
chartLabels = [];
|
|
2136
|
+
if (chart) { chart.destroy(); chart = null; }
|
|
2137
|
+
agentsEl.innerHTML = '';
|
|
2138
|
+
logEl.innerHTML = '';
|
|
2139
|
+
document.getElementById('m-stability').textContent = '--';
|
|
2140
|
+
document.getElementById('m-volatility').textContent = '--';
|
|
2141
|
+
document.getElementById('m-round').textContent = '--';
|
|
2142
|
+
document.getElementById('m-interventions').textContent = '0';
|
|
2143
|
+
|
|
2144
|
+
// Gather state overrides
|
|
2145
|
+
const stateOverrides = {};
|
|
2146
|
+
if (currentWorld.stateVariables) {
|
|
2147
|
+
currentWorld.stateVariables.forEach(sv => {
|
|
2148
|
+
const el = document.getElementById('sv-' + sv.id);
|
|
2149
|
+
if (!el) return;
|
|
2150
|
+
if (sv.type === 'number') stateOverrides[sv.id] = parseFloat(el.value);
|
|
2151
|
+
else if (sv.type === 'boolean') stateOverrides[sv.id] = el.classList.contains('on');
|
|
2152
|
+
else stateOverrides[sv.id] = el.value;
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
const selectedEngine = engineSelect.value;
|
|
2157
|
+
|
|
2158
|
+
try {
|
|
2159
|
+
if (selectedEngine === 'nv-sim') {
|
|
2160
|
+
// Built-in simulation
|
|
2161
|
+
const config = {
|
|
2162
|
+
worldId: currentWorld.id,
|
|
2163
|
+
stateOverrides,
|
|
2164
|
+
injectEvents: injectedEvents.length > 0 ? injectedEvents : undefined,
|
|
2165
|
+
rounds: parseInt(roundsSlider.value),
|
|
2166
|
+
};
|
|
2167
|
+
await fetch('/api/run-sim', {
|
|
2168
|
+
method: 'POST',
|
|
2169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2170
|
+
body: JSON.stringify(config),
|
|
2171
|
+
});
|
|
2172
|
+
} else {
|
|
2173
|
+
// Live adapter (external simulator)
|
|
2174
|
+
const payload = {
|
|
2175
|
+
adapterId: selectedEngine,
|
|
2176
|
+
worldId: currentWorld.id,
|
|
2177
|
+
stateOverrides,
|
|
2178
|
+
};
|
|
2179
|
+
await fetch('/api/run-live', {
|
|
2180
|
+
method: 'POST',
|
|
2181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2182
|
+
body: JSON.stringify(payload),
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
} catch (err) {
|
|
2186
|
+
addLog('Error starting simulation: ' + err.message, 'block');
|
|
2187
|
+
runBtn.disabled = false;
|
|
2188
|
+
runBtn.textContent = 'Run Simulation';
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
// ============================================
|
|
2193
|
+
// SSE — Live stream
|
|
2194
|
+
// ============================================
|
|
2195
|
+
function connectSSE() {
|
|
2196
|
+
const es = new EventSource('/events');
|
|
2197
|
+
es.onmessage = (e) => {
|
|
2198
|
+
try { handleEvent(JSON.parse(e.data)); } catch {}
|
|
2199
|
+
};
|
|
2200
|
+
es.onerror = () => {};
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
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
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
function addLog(msg, cls) {
|
|
2229
|
+
const entry = document.createElement('div');
|
|
2230
|
+
entry.style.cssText = 'font-size:10px;color:#666;padding:3px 0;';
|
|
2231
|
+
entry.textContent = msg;
|
|
2232
|
+
logEl.prepend(entry);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function addTraceRound(event) {
|
|
2236
|
+
const el = document.createElement('div');
|
|
2237
|
+
el.className = 'trace-round';
|
|
2238
|
+
|
|
2239
|
+
const phaseCls = event.phase === 'baseline' ? 'baseline' : 'governed';
|
|
2240
|
+
const volatilityPct = (event.maxVolatility * 100).toFixed(0);
|
|
2241
|
+
el.innerHTML = '<div class="trace-round-header" onclick="this.nextElementSibling.dataset.collapsed = this.nextElementSibling.dataset.collapsed === \\'true\\' ? \\'false\\' : \\'true\\'">' +
|
|
2242
|
+
'<span class="trace-phase ' + phaseCls + '">' + event.phase + '</span>' +
|
|
2243
|
+
'<span class="trace-round-label">Round ' + event.round + ' / ' + event.totalRounds + '</span>' +
|
|
2244
|
+
'<span class="trace-round-metrics">' +
|
|
2245
|
+
'<span>avg: ' + event.avgImpact.toFixed(2) + '</span>' +
|
|
2246
|
+
'<span>vol: ' + volatilityPct + '%</span>' +
|
|
2247
|
+
(event.interventionCount > 0 ? '<span style="color:#fbbf24">' + event.interventionCount + ' interventions</span>' : '') +
|
|
2248
|
+
'</span>' +
|
|
2249
|
+
'</div>';
|
|
2250
|
+
|
|
2251
|
+
let bodyHtml = '';
|
|
2252
|
+
|
|
2253
|
+
// 1) Narrative events injected this round
|
|
2254
|
+
const roundEvents = (typeof narrativeEventsByRound !== 'undefined' && narrativeEventsByRound[event.round]) || [];
|
|
2255
|
+
if (roundEvents.length > 0) {
|
|
2256
|
+
bodyHtml += '<div class="trace-section"><div class="trace-section-label event">Event Injection</div>';
|
|
2257
|
+
roundEvents.forEach(function(ev) {
|
|
2258
|
+
bodyHtml += '<div class="trace-event-item">' + ev.headline + ' <span class="trace-event-severity ' + ev.severity + '">' + ev.severity + '</span></div>';
|
|
2259
|
+
});
|
|
2260
|
+
bodyHtml += '</div><div class="trace-arrow">↓</div>';
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// 2) Agent reactions (cap at 30 to prevent DOM bloat with thousands of agents)
|
|
2264
|
+
var MAX_TRACE_AGENTS = 30;
|
|
2265
|
+
bodyHtml += '<div class="trace-section"><div class="trace-section-label agents">Agent Reactions (' + event.reactions.length + ')</div>';
|
|
2266
|
+
event.reactions.slice(0, MAX_TRACE_AGENTS).forEach(function(r) {
|
|
2267
|
+
const impactCls = r.impact >= 0 ? 'positive' : 'negative';
|
|
2268
|
+
bodyHtml += '<div class="trace-agent-item">' +
|
|
2269
|
+
'<span class="trace-agent-name">' + r.stakeholder_id + '</span>' +
|
|
2270
|
+
'<span class="trace-agent-action">' + r.reaction + '</span>' +
|
|
2271
|
+
'<span class="trace-agent-impact ' + impactCls + '">' + r.impact.toFixed(2) + '</span>' +
|
|
2272
|
+
'</div>';
|
|
2273
|
+
});
|
|
2274
|
+
if (event.reactions.length > MAX_TRACE_AGENTS) {
|
|
2275
|
+
var traceRemaining = event.reactions.length - MAX_TRACE_AGENTS;
|
|
2276
|
+
bodyHtml += '<div style="font-size:10px;color:#444;padding:3px 0 3px 11px;border-left:2px solid #3b82f6;margin-left:2px">+ ' + traceRemaining + ' more agents</div>';
|
|
2277
|
+
}
|
|
2278
|
+
bodyHtml += '</div>';
|
|
2279
|
+
|
|
2280
|
+
// 3) Governance interventions
|
|
2281
|
+
const governed = event.reactions.filter(function(r) { return r.verdict && r.verdict.status !== 'ALLOW'; });
|
|
2282
|
+
if (governed.length > 0) {
|
|
2283
|
+
bodyHtml += '<div class="trace-arrow">↓</div>';
|
|
2284
|
+
bodyHtml += '<div class="trace-section"><div class="trace-section-label governance">Governance Interventions</div>';
|
|
2285
|
+
governed.forEach(function(r) {
|
|
2286
|
+
bodyHtml += '<div class="trace-gov-item">' +
|
|
2287
|
+
'<span class="verdict ' + r.verdict.status + '">' + r.verdict.status + '</span>' +
|
|
2288
|
+
(r.verdict.ruleId ? '<span class="trace-gov-rule">[' + r.verdict.ruleId + ']</span>' : '') +
|
|
2289
|
+
'<span class="trace-gov-reason">' + r.stakeholder_id + ': ' + (r.verdict.reason || '') + '</span>' +
|
|
2290
|
+
'</div>';
|
|
2291
|
+
});
|
|
2292
|
+
bodyHtml += '</div>';
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// 4) Emergent dynamics
|
|
2296
|
+
if (event.dynamics && event.dynamics.length > 0) {
|
|
2297
|
+
bodyHtml += '<div class="trace-arrow">↓</div>';
|
|
2298
|
+
bodyHtml += '<div class="trace-section"><div class="trace-section-label" style="color:#7c3aed">Emergent Dynamics</div>';
|
|
2299
|
+
event.dynamics.forEach(function(d) {
|
|
2300
|
+
bodyHtml += '<div class="trace-dynamics">' + d + '</div>';
|
|
2301
|
+
});
|
|
2302
|
+
bodyHtml += '</div>';
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
const bodyEl = document.createElement('div');
|
|
2306
|
+
bodyEl.className = 'trace-body';
|
|
2307
|
+
bodyEl.dataset.collapsed = 'false';
|
|
2308
|
+
bodyEl.innerHTML = bodyHtml;
|
|
2309
|
+
el.appendChild(bodyEl);
|
|
2310
|
+
|
|
2311
|
+
logEl.prepend(el);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function renderAgents(reactions) {
|
|
2315
|
+
var MAX_AGENT_ROWS = 50;
|
|
2316
|
+
var shown = reactions.slice(0, MAX_AGENT_ROWS);
|
|
2317
|
+
var html = shown.map(r => {
|
|
2318
|
+
const pct = Math.abs(r.impact) * 50;
|
|
2319
|
+
const cls = r.impact >= 0 ? 'positive' : 'negative';
|
|
2320
|
+
const dir = r.impact >= 0 ? 'right:50%;width:' + pct + '%' : 'left:50%;width:' + pct + '%';
|
|
2321
|
+
const verdictHtml = r.verdict ? ' <span class="verdict ' + r.verdict.status + '">' + r.verdict.status + '</span>' : '';
|
|
2322
|
+
return '<div class="agent-row">' +
|
|
2323
|
+
'<span class="agent-name">' + r.stakeholder_id + verdictHtml + '</span>' +
|
|
2324
|
+
'<div class="impact-bar-bg"><div class="center-line"></div><div class="impact-bar ' + cls + '" style="' + dir + '"></div></div>' +
|
|
2325
|
+
'<span class="impact-val">' + r.impact.toFixed(2) + '</span>' +
|
|
2326
|
+
'</div>';
|
|
2327
|
+
}).join('');
|
|
2328
|
+
if (reactions.length > MAX_AGENT_ROWS) {
|
|
2329
|
+
var remaining = reactions.length - MAX_AGENT_ROWS;
|
|
2330
|
+
var avgImpact = reactions.slice(MAX_AGENT_ROWS).reduce(function(s, r) { return s + r.impact; }, 0) / remaining;
|
|
2331
|
+
html += '<div class="agent-row" style="color:#555;font-size:10px;justify-content:center">+ ' + remaining + ' more agents (avg impact: ' + avgImpact.toFixed(2) + ')</div>';
|
|
2332
|
+
}
|
|
2333
|
+
agentsEl.innerHTML = html;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function handleEvent(event) {
|
|
2337
|
+
if (event.type === 'meta') {
|
|
2338
|
+
statusEl.className = 'status live';
|
|
2339
|
+
statusEl.textContent = 'LIVE';
|
|
2340
|
+
// Show simulation source
|
|
2341
|
+
const src = event.source || 'nv-sim';
|
|
2342
|
+
if (src !== 'nv-sim') {
|
|
2343
|
+
traceSourceEl.textContent = '● ' + src.toUpperCase() + ' (LIVE)';
|
|
2344
|
+
traceSourceEl.style.color = '#4ade80';
|
|
2345
|
+
engineStatusEl.textContent = 'Streaming from ' + src;
|
|
2346
|
+
engineStatusEl.style.color = '#4ade80';
|
|
2347
|
+
} else {
|
|
2348
|
+
traceSourceEl.textContent = '';
|
|
2349
|
+
engineStatusEl.textContent = '';
|
|
2350
|
+
}
|
|
2351
|
+
addLog('Simulation started: ' + event.agents.length + ' agents, ' + event.totalRounds + ' rounds' + (src !== 'nv-sim' ? ' [source: ' + src + ']' : ''));
|
|
2352
|
+
resetShiftTracker();
|
|
2353
|
+
// Store narrative events by round for trace rendering
|
|
2354
|
+
narrativeEventsByRound = {};
|
|
2355
|
+
(event.narrativeEvents || []).forEach(function(ev) {
|
|
2356
|
+
if (!narrativeEventsByRound[ev.round]) narrativeEventsByRound[ev.round] = [];
|
|
2357
|
+
narrativeEventsByRound[ev.round].push(ev);
|
|
2358
|
+
});
|
|
2359
|
+
function parseRuleTitle(id, text) {
|
|
2360
|
+
var parts = text.split(/\s*[—–-]{1,}\s*/);
|
|
2361
|
+
var title = parts.length > 1 ? parts[0].trim() : id.replace(/[-_]/g, ' ');
|
|
2362
|
+
var desc = parts.length > 1 ? parts.slice(1).join(' — ').trim() : text;
|
|
2363
|
+
title = title.replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
|
2364
|
+
return { title: title, desc: desc };
|
|
2365
|
+
}
|
|
2366
|
+
function generateWhy(text, type) {
|
|
2367
|
+
var t = text.toLowerCase();
|
|
2368
|
+
if (t.includes('liquidity') || t.includes('drain')) return 'Prevents system collapse from liquidity crises';
|
|
2369
|
+
if (t.includes('panic') || t.includes('cascade')) return 'Stops runaway feedback loops during market shocks';
|
|
2370
|
+
if (t.includes('leverage')) return 'Limits systemic risk from over-leveraged positions';
|
|
2371
|
+
if (t.includes('price') || t.includes('pricing')) return 'Stabilizes pricing mechanisms during volatility';
|
|
2372
|
+
if (t.includes('sentiment') || t.includes('consumer')) return 'Tracks behavioral feedback between price and confidence';
|
|
2373
|
+
if (t.includes('military') || t.includes('escalat')) return 'Models how escalation constrains available options';
|
|
2374
|
+
if (t.includes('diplomatic') || t.includes('window')) return 'Preserves negotiation pathways before they close';
|
|
2375
|
+
if (t.includes('grid') || t.includes('capacity')) return 'Prevents infrastructure overload from demand surges';
|
|
2376
|
+
if (t.includes('election') || t.includes('political')) return 'Captures how political pressure amplifies responses';
|
|
2377
|
+
if (t.includes('supply') || t.includes('energy') || t.includes('oil')) return 'Guards against cascading supply chain disruptions';
|
|
2378
|
+
if (t.includes('fraud') || t.includes('suspicious')) return 'Detects and contains anomalous behavior patterns';
|
|
2379
|
+
if (t.includes('withdraw') || t.includes('limit')) return 'Constrains individual actions to protect system stability';
|
|
2380
|
+
if (type === 'gate') return 'Blocks dangerous actions before they propagate';
|
|
2381
|
+
if (type === 'warning') return 'Provides early warning before thresholds are breached';
|
|
2382
|
+
return 'Maintains system integrity under stress conditions';
|
|
2383
|
+
}
|
|
2384
|
+
// Reset per-rule impact counters
|
|
2385
|
+
ruleImpactTracker = {};
|
|
2386
|
+
activeInvEl.innerHTML = event.invariants.map(function(inv) {
|
|
2387
|
+
var parsed = parseRuleTitle(inv.id, inv.description);
|
|
2388
|
+
var why = generateWhy(inv.description, 'invariant');
|
|
2389
|
+
var isUser = inv.source === 'user';
|
|
2390
|
+
var isFull = inv.enforcement === 'full';
|
|
2391
|
+
var enfLabel = isFull ? 'Fully enforced across system' : 'Advisory only';
|
|
2392
|
+
var enfIcon = isFull ? '✓' : '⚠';
|
|
2393
|
+
var sourceTag = isUser ? '<span class="rule-source-tag">USER RULE</span>' : '';
|
|
2394
|
+
ruleImpactTracker[inv.id] = { blocks: 0, label: parsed.title };
|
|
2395
|
+
return '<div class="rule-card type-invariant' + (isUser ? ' user-rule' : '') + '" data-rule-id="' + inv.id + '">' +
|
|
2396
|
+
'<div class="rule-header"><span class="rule-icon">🟢</span><span class="rule-title">' + parsed.title + '</span>' + sourceTag + '</div>' +
|
|
2397
|
+
'<div class="rule-desc">' + parsed.desc + '</div>' +
|
|
2398
|
+
'<div class="rule-meta">Invariant • ' + enfIcon + ' ' + enfLabel + '</div>' +
|
|
2399
|
+
'<div class="rule-why">' + why + '</div>' +
|
|
2400
|
+
'<div class="rule-impact" data-impact-id="' + inv.id + '"></div>' +
|
|
2401
|
+
'</div>';
|
|
2402
|
+
}).join('') + event.gates.map(function(g) {
|
|
2403
|
+
var isCritical = g.severity === 'critical';
|
|
2404
|
+
var typeClass = isCritical ? 'type-gate' : 'type-warning';
|
|
2405
|
+
var icon = isCritical ? '🔴' : '🟡';
|
|
2406
|
+
var typeLabel = isCritical ? 'Gate' : 'Warning';
|
|
2407
|
+
var effect = isCritical ? 'Blocks actions' : 'Signals risk';
|
|
2408
|
+
var why = generateWhy(g.label + ' ' + (g.condition || ''), isCritical ? 'gate' : 'warning');
|
|
2409
|
+
ruleImpactTracker[g.id] = { blocks: 0, label: g.label };
|
|
2410
|
+
return '<div class="rule-card ' + typeClass + '" data-rule-id="' + g.id + '">' +
|
|
2411
|
+
'<div class="rule-header"><span class="rule-icon">' + icon + '</span><span class="rule-title">' + g.label + '</span></div>' +
|
|
2412
|
+
'<div class="rule-desc">' + (g.condition || g.label) + '</div>' +
|
|
2413
|
+
'<div class="rule-meta">' + typeLabel + ' • ' + effect + '</div>' +
|
|
2414
|
+
'<div class="rule-why">' + why + '</div>' +
|
|
2415
|
+
'<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
|
|
2416
|
+
'</div>';
|
|
2417
|
+
}).join('');
|
|
2418
|
+
initChart();
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
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();
|
|
2429
|
+
|
|
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');
|
|
2434
|
+
|
|
2435
|
+
totalInterventions += event.interventionCount;
|
|
2436
|
+
document.getElementById('m-interventions').textContent = totalInterventions;
|
|
2437
|
+
|
|
2438
|
+
// Track system shifts for the card
|
|
2439
|
+
trackShift(event);
|
|
2440
|
+
|
|
2441
|
+
// Render structured trace entry instead of flat log line
|
|
2442
|
+
addTraceRound(event);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
if (event.type === 'complete') {
|
|
2446
|
+
statusEl.className = 'status complete';
|
|
2447
|
+
statusEl.textContent = 'COMPLETE';
|
|
2448
|
+
const r = event.result;
|
|
2449
|
+
if (r.governed) {
|
|
2450
|
+
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) + '%');
|
|
2453
|
+
renderSystemShift(r);
|
|
2454
|
+
renderRuleImpacts(r);
|
|
2455
|
+
renderEnforcementClassification(r.enforcementClassification || []);
|
|
2456
|
+
lastSimResult = {
|
|
2457
|
+
stability: r.governed.metrics.stabilityScore,
|
|
2458
|
+
volatility: r.governed.metrics.maxVolatility,
|
|
2459
|
+
collapseProbability: r.governed.metrics.collapseProbability,
|
|
2460
|
+
governanceEffectiveness: r.comparison.governanceEffectiveness,
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
runBtn.disabled = false;
|
|
2464
|
+
runBtn.textContent = 'Run Simulation';
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// ============================================
|
|
2469
|
+
// PER-RULE IMPACT RENDERING
|
|
2470
|
+
// ============================================
|
|
2471
|
+
function renderRuleImpacts(result) {
|
|
2472
|
+
var totalBlocks = shiftTracker.blocks;
|
|
2473
|
+
var cascadeAvoided = result && result.governed && result.baseline &&
|
|
2474
|
+
result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
|
|
2475
|
+
|
|
2476
|
+
Object.keys(ruleImpactTracker).forEach(function(ruleId) {
|
|
2477
|
+
var tracker = ruleImpactTracker[ruleId];
|
|
2478
|
+
var el = document.querySelector('[data-impact-id="' + ruleId + '"]');
|
|
2479
|
+
if (!el) return;
|
|
2480
|
+
|
|
2481
|
+
var html = '';
|
|
2482
|
+
if (tracker.blocks > 0) {
|
|
2483
|
+
html += '<span class="impact-stat">' + tracker.blocks + ' action' + (tracker.blocks > 1 ? 's' : '') + ' blocked</span>';
|
|
2484
|
+
if (cascadeAvoided) {
|
|
2485
|
+
html += ' <span class="impact-label">→ cascade avoided</span>';
|
|
2486
|
+
} else {
|
|
2487
|
+
html += ' <span class="impact-label">→ behavior modified</span>';
|
|
2488
|
+
}
|
|
2489
|
+
} else {
|
|
2490
|
+
// Rule was present but didn't fire — still useful info
|
|
2491
|
+
html += '<span class="impact-label">No violations detected — agents complied</span>';
|
|
2492
|
+
}
|
|
2493
|
+
el.innerHTML = html;
|
|
2494
|
+
el.classList.add('visible');
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// ============================================
|
|
2499
|
+
// ENFORCEMENT CLASSIFICATION — Full vs Advisory
|
|
2500
|
+
// ============================================
|
|
2501
|
+
function renderEnforcementClassification(entries) {
|
|
2502
|
+
if (!entries || entries.length === 0) return;
|
|
2503
|
+
entries.forEach(function(entry) {
|
|
2504
|
+
var card = document.querySelector('[data-rule-id="' + entry.id + '"]');
|
|
2505
|
+
if (!card) return;
|
|
2506
|
+
var metaEl = card.querySelector('.rule-meta');
|
|
2507
|
+
if (!metaEl) return;
|
|
2508
|
+
if (entry.level === 'full' && entry.fired) {
|
|
2509
|
+
metaEl.innerHTML = 'Invariant • ✓ Fully enforced • <span style="color:#22c55e;font-weight:600">FIRED</span>';
|
|
2510
|
+
card.style.borderLeftColor = '#22c55e';
|
|
2511
|
+
} else if (entry.level === 'full' && !entry.fired) {
|
|
2512
|
+
metaEl.innerHTML = 'Invariant • ✓ Fully enforced • <span style="color:var(--text-muted)">standby</span>';
|
|
2513
|
+
} else if (entry.level === 'advisory') {
|
|
2514
|
+
metaEl.innerHTML = 'Invariant • ⚠ Advisory only • <span style="color:var(--text-muted)">monitored</span>';
|
|
2515
|
+
card.style.borderLeftColor = '#6b7280';
|
|
2516
|
+
card.style.opacity = '0.75';
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// ============================================
|
|
2522
|
+
// SYSTEM SHIFT CARD — The Demo Moment
|
|
2523
|
+
// ============================================
|
|
2524
|
+
const ssCard = document.getElementById('system-shift');
|
|
2525
|
+
const ssRuleEl = document.getElementById('ss-rule');
|
|
2526
|
+
const ssAdaptRateEl = document.getElementById('ss-adapt-rate');
|
|
2527
|
+
const ssAdaptDescEl = document.getElementById('ss-adapt-desc');
|
|
2528
|
+
const ssShiftsEl = document.getElementById('ss-shifts');
|
|
2529
|
+
const ssPatternsEl = document.getElementById('ss-patterns');
|
|
2530
|
+
const ssImpactsEl = document.getElementById('ss-impacts');
|
|
2531
|
+
const ssNarrativeEl = document.getElementById('ss-narrative');
|
|
2532
|
+
|
|
2533
|
+
// 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
|
+
});
|
|
2538
|
+
|
|
2539
|
+
let shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
|
|
2540
|
+
|
|
2541
|
+
function resetShiftTracker() {
|
|
2542
|
+
shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
|
|
2543
|
+
ssCard.classList.remove('visible');
|
|
2544
|
+
var rawToggle = document.getElementById('ss-raw-toggle');
|
|
2545
|
+
var rawDetail = document.getElementById('ss-raw-detail');
|
|
2546
|
+
if (rawToggle) rawToggle.classList.remove('open');
|
|
2547
|
+
if (rawDetail) rawDetail.classList.remove('open');
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
function trackShift(event) {
|
|
2551
|
+
if (!event.reactions) return;
|
|
2552
|
+
const governed = event.reactions.filter(function(r) { return r.verdict && r.verdict.status !== 'ALLOW'; });
|
|
2553
|
+
shiftTracker.blocks += governed.length;
|
|
2554
|
+
shiftTracker.total += event.reactions.length;
|
|
2555
|
+
governed.forEach(function(r) {
|
|
2556
|
+
const key = r.verdict.status + ': ' + (r.reaction || 'adapted');
|
|
2557
|
+
shiftTracker.shifts[key] = (shiftTracker.shifts[key] || 0) + 1;
|
|
2558
|
+
// Track per-rule impacts for card display
|
|
2559
|
+
var ruleId = r.verdict.ruleId || '';
|
|
2560
|
+
if (ruleId && ruleImpactTracker[ruleId]) {
|
|
2561
|
+
ruleImpactTracker[ruleId].blocks++;
|
|
2562
|
+
} else {
|
|
2563
|
+
// Try to match by pattern — governance may report with NV- prefix
|
|
2564
|
+
Object.keys(ruleImpactTracker).forEach(function(k) {
|
|
2565
|
+
if (ruleId.includes(k) || (r.verdict.reason && r.verdict.reason.toLowerCase().includes(ruleImpactTracker[k].label.toLowerCase()))) {
|
|
2566
|
+
ruleImpactTracker[k].blocks++;
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
// Store raw governed reactions for detail view
|
|
2571
|
+
shiftTracker.rawGoverned.push({
|
|
2572
|
+
agent: r.stakeholder_id,
|
|
2573
|
+
action: r.reaction,
|
|
2574
|
+
status: r.verdict.status,
|
|
2575
|
+
reason: r.verdict.reason || '',
|
|
2576
|
+
round: event.round,
|
|
2577
|
+
});
|
|
2578
|
+
});
|
|
2579
|
+
if (event.phase === 'baseline') shiftTracker.baselineVol = Math.max(shiftTracker.baselineVol, event.maxVolatility || 0);
|
|
2580
|
+
if (event.phase === 'governed') shiftTracker.governedVol = Math.max(shiftTracker.governedVol, event.maxVolatility || 0);
|
|
2581
|
+
if (event.dynamics) {
|
|
2582
|
+
event.dynamics.forEach(function(d) {
|
|
2583
|
+
if (shiftTracker.patterns.indexOf(d) === -1) shiftTracker.patterns.push(d);
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
function renderSystemShift(result) {
|
|
2589
|
+
if (shiftTracker.blocks === 0) return;
|
|
2590
|
+
|
|
2591
|
+
var adaptRate = shiftTracker.total > 0 ? Math.round((shiftTracker.blocks / shiftTracker.total) * 100) : 0;
|
|
2592
|
+
|
|
2593
|
+
// Determine rule label from world
|
|
2594
|
+
var ruleName = 'Governance Rules Active';
|
|
2595
|
+
if (currentWorld && currentWorld.invariants) {
|
|
2596
|
+
var names = currentWorld.invariants.map(function(inv) { return inv.description || inv.id; });
|
|
2597
|
+
ruleName = names.slice(0, 2).join(' + ');
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
ssRuleEl.textContent = ruleName;
|
|
2601
|
+
ssAdaptRateEl.textContent = adaptRate + '%';
|
|
2602
|
+
ssAdaptDescEl.textContent = 'adaptation across ' + shiftTracker.total.toLocaleString() + ' agents';
|
|
2603
|
+
|
|
2604
|
+
// Scale line — proves this is real
|
|
2605
|
+
var scaleEl = document.getElementById('ss-scale');
|
|
2606
|
+
scaleEl.innerHTML = '<strong>' + shiftTracker.blocks.toLocaleString() + '</strong> actions reshaped out of <strong>' + shiftTracker.total.toLocaleString() + '</strong> total';
|
|
2607
|
+
|
|
2608
|
+
// Shifts — show top 5 by count, collapse the rest
|
|
2609
|
+
var shiftHtml = '';
|
|
2610
|
+
var shiftKeys = Object.keys(shiftTracker.shifts).sort(function(a, b) {
|
|
2611
|
+
return shiftTracker.shifts[b] - shiftTracker.shifts[a];
|
|
2612
|
+
});
|
|
2613
|
+
var MAX_SHIFTS = 5;
|
|
2614
|
+
shiftKeys.slice(0, MAX_SHIFTS).forEach(function(k) {
|
|
2615
|
+
shiftHtml += '<div class="ss-shift-item"><span class="ss-shift-arrow">→</span> ' + k + ' (' + shiftTracker.shifts[k] + ' agents)</div>';
|
|
2616
|
+
});
|
|
2617
|
+
if (shiftKeys.length > MAX_SHIFTS) {
|
|
2618
|
+
var remaining = shiftKeys.slice(MAX_SHIFTS).reduce(function(sum, k) { return sum + shiftTracker.shifts[k]; }, 0);
|
|
2619
|
+
shiftHtml += '<div class="ss-shift-item" style="color:#555">+ ' + (shiftKeys.length - MAX_SHIFTS) + ' more patterns (' + remaining + ' agents)</div>';
|
|
2620
|
+
}
|
|
2621
|
+
ssShiftsEl.innerHTML = shiftHtml;
|
|
2622
|
+
|
|
2623
|
+
// Patterns
|
|
2624
|
+
var patternHtml = '';
|
|
2625
|
+
if (shiftTracker.patterns.length > 0) {
|
|
2626
|
+
shiftTracker.patterns.forEach(function(p) {
|
|
2627
|
+
patternHtml += '<span class="ss-pattern-tag">' + p + '</span>';
|
|
2628
|
+
});
|
|
2629
|
+
} else {
|
|
2630
|
+
// Infer patterns from data
|
|
2631
|
+
if (shiftTracker.blocks >= 3) patternHtml += '<span class="ss-pattern-tag">Coordinated Holding</span>';
|
|
2632
|
+
if (shiftTracker.blocks >= 2) patternHtml += '<span class="ss-pattern-tag">Panic Suppression</span>';
|
|
2633
|
+
if (shiftTracker.governedVol < shiftTracker.baselineVol) patternHtml += '<span class="ss-pattern-tag">Stability Shift</span>';
|
|
2634
|
+
}
|
|
2635
|
+
ssPatternsEl.innerHTML = patternHtml || '<span style="font-size:11px;color:#555">No emergent patterns detected</span>';
|
|
2636
|
+
|
|
2637
|
+
// Impact
|
|
2638
|
+
var impactHtml = '';
|
|
2639
|
+
var volDelta = shiftTracker.governedVol - shiftTracker.baselineVol;
|
|
2640
|
+
impactHtml += '<div class="ss-impact-row"><span>Volatility</span><span class="ss-impact-delta' + (volDelta > 0 ? ' negative' : '') + '">' +
|
|
2641
|
+
(shiftTracker.baselineVol * 100).toFixed(0) + '% → ' + (shiftTracker.governedVol * 100).toFixed(0) + '%</span></div>';
|
|
2642
|
+
|
|
2643
|
+
if (result && result.governed && result.baseline) {
|
|
2644
|
+
var stabDelta = result.governed.metrics.stabilityScore - result.baseline.metrics.stabilityScore;
|
|
2645
|
+
impactHtml += '<div class="ss-impact-row"><span>Stability</span><span class="ss-impact-delta">' +
|
|
2646
|
+
(result.baseline.metrics.stabilityScore * 100).toFixed(0) + '% → ' + (result.governed.metrics.stabilityScore * 100).toFixed(0) + '%</span></div>';
|
|
2647
|
+
|
|
2648
|
+
var cascadeAvoided = result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
|
|
2649
|
+
if (cascadeAvoided) {
|
|
2650
|
+
impactHtml += '<div class="ss-impact-row"><span>Cascade</span><span class="ss-impact-delta">Avoided</span></div>';
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
ssImpactsEl.innerHTML = impactHtml;
|
|
2654
|
+
|
|
2655
|
+
// Narrative — tight cause → shift → outcome
|
|
2656
|
+
var topShift = shiftKeys.length > 0 ? shiftKeys[0].split(': ')[1] || 'adapted' : 'adapted';
|
|
2657
|
+
var narrative = '';
|
|
2658
|
+
|
|
2659
|
+
// What was the rule?
|
|
2660
|
+
narrative += ruleName + '. ';
|
|
2661
|
+
|
|
2662
|
+
// What shifted?
|
|
2663
|
+
narrative += shiftTracker.blocks + ' of ' + shiftTracker.total + ' agents reorganized';
|
|
2664
|
+
if (topShift !== 'adapted') narrative += ' — most shifted to ' + topShift;
|
|
2665
|
+
narrative += '. ';
|
|
2666
|
+
|
|
2667
|
+
// What emerged?
|
|
2668
|
+
if (shiftTracker.patterns.length > 0) {
|
|
2669
|
+
narrative += 'Pattern: ' + shiftTracker.patterns.slice(0, 2).join(', ') + '. ';
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// What was the outcome?
|
|
2673
|
+
if (volDelta < 0) narrative += 'Volatility dropped ' + Math.abs(volDelta * 100).toFixed(0) + '%.';
|
|
2674
|
+
else if (volDelta === 0) narrative += 'System held steady.';
|
|
2675
|
+
else narrative += 'Volatility increased ' + (volDelta * 100).toFixed(0) + '% — rules need tuning.';
|
|
2676
|
+
|
|
2677
|
+
ssNarrativeEl.textContent = narrative;
|
|
2678
|
+
|
|
2679
|
+
// Raw detail — virtualized list of governed actions (collapsed by default)
|
|
2680
|
+
var rawListEl = document.getElementById('ss-raw-list');
|
|
2681
|
+
var MAX_RAW = 100;
|
|
2682
|
+
var rawItems = shiftTracker.rawGoverned.slice(0, MAX_RAW);
|
|
2683
|
+
var rawHtml = rawItems.map(function(r) {
|
|
2684
|
+
return '<div class="ss-raw-item">' +
|
|
2685
|
+
'<span class="raw-agent">' + r.agent + '</span>' +
|
|
2686
|
+
'<span class="raw-action">R' + r.round + ': ' + r.action + '</span>' +
|
|
2687
|
+
'<span class="raw-verdict ' + r.status + '">' + r.status + '</span>' +
|
|
2688
|
+
'</div>';
|
|
2689
|
+
}).join('');
|
|
2690
|
+
if (shiftTracker.rawGoverned.length > MAX_RAW) {
|
|
2691
|
+
rawHtml += '<div style="font-size:10px;color:#444;padding:4px 0">+ ' + (shiftTracker.rawGoverned.length - MAX_RAW) + ' more governed actions</div>';
|
|
2692
|
+
}
|
|
2693
|
+
rawListEl.innerHTML = rawHtml;
|
|
2694
|
+
|
|
2695
|
+
ssCard.classList.add('visible');
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// ============================================
|
|
2699
|
+
// WORLD VARIANTS — Save / Load / Delete
|
|
2700
|
+
// ============================================
|
|
2701
|
+
const variantListEl = document.getElementById('variant-list');
|
|
2702
|
+
const saveBtn = document.getElementById('save-btn');
|
|
2703
|
+
const saveForm = document.getElementById('save-form');
|
|
2704
|
+
const confirmSaveBtn = document.getElementById('confirm-save-btn');
|
|
2705
|
+
const cancelSaveBtn = document.getElementById('cancel-save-btn');
|
|
2706
|
+
const variantNameInput = document.getElementById('variant-name');
|
|
2707
|
+
const variantDescInput = document.getElementById('variant-desc');
|
|
2708
|
+
let lastSimResult = null;
|
|
2709
|
+
|
|
2710
|
+
saveBtn.addEventListener('click', () => {
|
|
2711
|
+
saveForm.style.display = saveForm.style.display === 'none' ? '' : 'none';
|
|
2712
|
+
variantNameInput.focus();
|
|
2713
|
+
});
|
|
2714
|
+
|
|
2715
|
+
cancelSaveBtn.addEventListener('click', () => {
|
|
2716
|
+
saveForm.style.display = 'none';
|
|
2717
|
+
});
|
|
2718
|
+
|
|
2719
|
+
confirmSaveBtn.addEventListener('click', async () => {
|
|
2720
|
+
const name = variantNameInput.value.trim();
|
|
2721
|
+
if (!name || !currentWorld) return;
|
|
2722
|
+
|
|
2723
|
+
// Gather current state overrides
|
|
2724
|
+
const stateOverrides = {};
|
|
2725
|
+
if (currentWorld.stateVariables) {
|
|
2726
|
+
currentWorld.stateVariables.forEach(sv => {
|
|
2727
|
+
const el = document.getElementById('sv-' + sv.id);
|
|
2728
|
+
if (!el) return;
|
|
2729
|
+
if (sv.type === 'number') stateOverrides[sv.id] = parseFloat(el.value);
|
|
2730
|
+
else if (sv.type === 'boolean') stateOverrides[sv.id] = el.classList.contains('on');
|
|
2731
|
+
else stateOverrides[sv.id] = el.value;
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
const payload = {
|
|
2736
|
+
name,
|
|
2737
|
+
description: variantDescInput.value.trim(),
|
|
2738
|
+
baseWorld: currentWorld.id,
|
|
2739
|
+
stateOverrides,
|
|
2740
|
+
events: injectedEvents.slice(),
|
|
2741
|
+
rounds: parseInt(roundsSlider.value),
|
|
2742
|
+
lastResult: lastSimResult,
|
|
2743
|
+
};
|
|
2744
|
+
|
|
2745
|
+
try {
|
|
2746
|
+
const resp = await fetch('/api/save-variant', {
|
|
2747
|
+
method: 'POST',
|
|
2748
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2749
|
+
body: JSON.stringify(payload),
|
|
2750
|
+
});
|
|
2751
|
+
const data = await resp.json();
|
|
2752
|
+
if (data.status === 'saved') {
|
|
2753
|
+
addLog('Variant saved: ' + data.variant.name + ' (' + data.variant.id + ')', '');
|
|
2754
|
+
saveForm.style.display = 'none';
|
|
2755
|
+
variantNameInput.value = '';
|
|
2756
|
+
variantDescInput.value = '';
|
|
2757
|
+
await loadVariants();
|
|
2758
|
+
}
|
|
2759
|
+
} catch (err) {
|
|
2760
|
+
addLog('Error saving variant: ' + err.message, 'block');
|
|
2761
|
+
}
|
|
2762
|
+
});
|
|
2763
|
+
|
|
2764
|
+
async function loadVariants() {
|
|
2765
|
+
try {
|
|
2766
|
+
const resp = await fetch('/api/variants');
|
|
2767
|
+
const data = await resp.json();
|
|
2768
|
+
renderVariants(data.variants || []);
|
|
2769
|
+
} catch {}
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
function renderVariants(variants) {
|
|
2773
|
+
if (variants.length === 0) {
|
|
2774
|
+
variantListEl.innerHTML = '<div style="font-size:11px;color:#333">No saved variants yet</div>';
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
variantListEl.innerHTML = variants.map(v => {
|
|
2778
|
+
const resultHtml = v.lastResult
|
|
2779
|
+
? '<span class="vresult">Stability: ' + (v.lastResult.stability * 100).toFixed(0) + '% | Effectiveness: ' + (v.lastResult.governanceEffectiveness * 100).toFixed(0) + '%</span>'
|
|
2780
|
+
: '<span style="color:#555">Not yet run</span>';
|
|
2781
|
+
return '<div class="variant-card" data-vid="' + v.id + '">' +
|
|
2782
|
+
'<div class="vname">' + v.name + '</div>' +
|
|
2783
|
+
(v.description ? '<div class="vdesc">' + v.description + '</div>' : '') +
|
|
2784
|
+
'<span class="vbase">' + v.baseWorld + '</span>' +
|
|
2785
|
+
'<div class="vmeta">' + resultHtml + ' | ' + v.events.length + ' events | ' + v.rounds + ' rounds</div>' +
|
|
2786
|
+
'<span class="vdelete" data-vid="' + v.id + '">delete</span>' +
|
|
2787
|
+
'</div>';
|
|
2788
|
+
}).join('');
|
|
2789
|
+
|
|
2790
|
+
// Bind load handlers
|
|
2791
|
+
variantListEl.querySelectorAll('.variant-card').forEach(card => {
|
|
2792
|
+
card.addEventListener('click', (e) => {
|
|
2793
|
+
if (e.target.classList.contains('vdelete')) return;
|
|
2794
|
+
const vid = card.dataset.vid;
|
|
2795
|
+
const v = variants.find(x => x.id === vid);
|
|
2796
|
+
if (v) loadVariant(v);
|
|
2797
|
+
});
|
|
2798
|
+
});
|
|
2799
|
+
|
|
2800
|
+
// Bind delete handlers
|
|
2801
|
+
variantListEl.querySelectorAll('.vdelete').forEach(el => {
|
|
2802
|
+
el.addEventListener('click', async (e) => {
|
|
2803
|
+
e.stopPropagation();
|
|
2804
|
+
const vid = el.dataset.vid;
|
|
2805
|
+
if (!confirm('Delete variant "' + vid + '"?')) return;
|
|
2806
|
+
try {
|
|
2807
|
+
await fetch('/api/delete-variant/' + vid, { method: 'DELETE' });
|
|
2808
|
+
addLog('Variant deleted: ' + vid, '');
|
|
2809
|
+
await loadVariants();
|
|
2810
|
+
} catch {}
|
|
2811
|
+
});
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function loadVariant(variant) {
|
|
2816
|
+
// Set world
|
|
2817
|
+
selectWorld(variant.baseWorld);
|
|
2818
|
+
|
|
2819
|
+
// Apply state overrides
|
|
2820
|
+
if (variant.stateOverrides && currentWorld && currentWorld.stateVariables) {
|
|
2821
|
+
currentWorld.stateVariables.forEach(sv => {
|
|
2822
|
+
if (sv.id in variant.stateOverrides) {
|
|
2823
|
+
const el = document.getElementById('sv-' + sv.id);
|
|
2824
|
+
if (!el) return;
|
|
2825
|
+
if (sv.type === 'boolean') {
|
|
2826
|
+
const val = variant.stateOverrides[sv.id];
|
|
2827
|
+
if (val && !el.classList.contains('on')) el.classList.add('on');
|
|
2828
|
+
if (!val && el.classList.contains('on')) el.classList.remove('on');
|
|
2829
|
+
} else {
|
|
2830
|
+
el.value = variant.stateOverrides[sv.id];
|
|
2831
|
+
// Update value display for sliders
|
|
2832
|
+
const valEl = document.getElementById('sv-val-' + sv.id);
|
|
2833
|
+
if (valEl) valEl.textContent = variant.stateOverrides[sv.id];
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// Set events
|
|
2840
|
+
injectedEvents = variant.events.slice();
|
|
2841
|
+
renderInjectedEvents();
|
|
2842
|
+
|
|
2843
|
+
// Set rounds
|
|
2844
|
+
roundsSlider.value = Math.min(variant.rounds, 12);
|
|
2845
|
+
roundsVal.textContent = Math.min(variant.rounds, 12);
|
|
2846
|
+
|
|
2847
|
+
addLog('Loaded variant: ' + variant.name, '');
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// ============================================
|
|
2851
|
+
// CAPTURE LAST RESULT FOR VARIANT SAVING
|
|
2852
|
+
// ============================================
|
|
2853
|
+
// (override handleEvent to capture results)
|
|
2854
|
+
const _origHandleEvent = handleEvent;
|
|
2855
|
+
handleEvent = function(event) {
|
|
2856
|
+
_origHandleEvent(event);
|
|
2857
|
+
if (event.type === 'complete' && event.result && event.result.governed) {
|
|
2858
|
+
lastSimResult = {
|
|
2859
|
+
stability: event.result.governed.metrics.stabilityScore,
|
|
2860
|
+
volatility: event.result.governed.metrics.maxVolatility,
|
|
2861
|
+
collapseProbability: event.result.governed.metrics.collapseProbability,
|
|
2862
|
+
governanceEffectiveness: event.result.comparison.governanceEffectiveness,
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
|
|
2867
|
+
// ============================================
|
|
2868
|
+
// THEME TOGGLE — Light / Dark mode
|
|
2869
|
+
// ============================================
|
|
2870
|
+
const themeToggleBtn = document.getElementById('theme-toggle');
|
|
2871
|
+
function applyTheme(theme) {
|
|
2872
|
+
if (theme === 'light') {
|
|
2873
|
+
document.body.classList.add('light');
|
|
2874
|
+
themeToggleBtn.textContent = 'Dark Mode';
|
|
2875
|
+
} else {
|
|
2876
|
+
document.body.classList.remove('light');
|
|
2877
|
+
themeToggleBtn.textContent = 'Light Mode';
|
|
2878
|
+
}
|
|
2879
|
+
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
|
+
}
|
|
2893
|
+
themeToggleBtn.addEventListener('click', () => {
|
|
2894
|
+
const current = document.body.classList.contains('light') ? 'light' : 'dark';
|
|
2895
|
+
applyTheme(current === 'light' ? 'dark' : 'light');
|
|
2896
|
+
});
|
|
2897
|
+
// Restore saved theme
|
|
2898
|
+
const savedTheme = localStorage.getItem('nv-theme');
|
|
2899
|
+
if (savedTheme) applyTheme(savedTheme);
|
|
2900
|
+
|
|
2901
|
+
// ============================================
|
|
2902
|
+
// PLAIN-ENGLISH RULE EDITOR
|
|
2903
|
+
// ============================================
|
|
2904
|
+
const ruleInput = document.getElementById('rule-input');
|
|
2905
|
+
const parseRulesBtn = document.getElementById('parse-rules-btn');
|
|
2906
|
+
const parsedRulesEl = document.getElementById('parsed-rules');
|
|
2907
|
+
const ruleStatusEl = document.getElementById('rule-status');
|
|
2908
|
+
let parsedRuleData = [];
|
|
2909
|
+
|
|
2910
|
+
parseRulesBtn.addEventListener('click', async () => {
|
|
2911
|
+
const text = ruleInput.value.trim();
|
|
2912
|
+
if (!text) return;
|
|
2913
|
+
|
|
2914
|
+
parseRulesBtn.disabled = true;
|
|
2915
|
+
parseRulesBtn.textContent = 'Parsing...';
|
|
2916
|
+
ruleStatusEl.textContent = '';
|
|
2917
|
+
ruleStatusEl.className = 'rule-status';
|
|
2918
|
+
|
|
2919
|
+
try {
|
|
2920
|
+
const resp = await fetch('/api/parse-rules', {
|
|
2921
|
+
method: 'POST',
|
|
2922
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2923
|
+
body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
|
|
2924
|
+
});
|
|
2925
|
+
const data = await resp.json();
|
|
2926
|
+
|
|
2927
|
+
if (data.error) {
|
|
2928
|
+
ruleStatusEl.textContent = data.error;
|
|
2929
|
+
ruleStatusEl.className = 'rule-status error';
|
|
2930
|
+
parsedRulesEl.innerHTML = '';
|
|
2931
|
+
parsedRuleData = [];
|
|
2932
|
+
} else {
|
|
2933
|
+
parsedRuleData = data.rules || [];
|
|
2934
|
+
parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
|
|
2935
|
+
const enfType = r.enforcement || 'block';
|
|
2936
|
+
const iconMap = { block: '🔴', allow: '🟢', modify: '🔵', warn: '🟡', pause: '🟡' };
|
|
2937
|
+
const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
|
|
2938
|
+
const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
|
|
2939
|
+
const icon = iconMap[enfType] || '🟢';
|
|
2940
|
+
const label = labelMap[enfType] || 'Rule';
|
|
2941
|
+
const effect = effectMap[enfType] || 'Active';
|
|
2942
|
+
return '<div class="parsed-rule enforcement-' + enfType + '">' +
|
|
2943
|
+
'<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
|
|
2944
|
+
'<div class="pr-desc">' + r.description + '</div>' +
|
|
2945
|
+
'<div class="pr-patterns">' + effect + ' • Matches: ' + r.intent_patterns.join(', ') + '</div>' +
|
|
2946
|
+
'</div>';
|
|
2947
|
+
}).join('');
|
|
2948
|
+
|
|
2949
|
+
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>';
|
|
2951
|
+
document.getElementById('apply-rules-btn').addEventListener('click', async () => {
|
|
2952
|
+
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';
|
|
2962
|
+
}
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
|
|
2965
|
+
ruleStatusEl.className = 'rule-status error';
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2968
|
+
ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click Apply.';
|
|
2969
|
+
ruleStatusEl.className = 'rule-status success';
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
ruleStatusEl.textContent = 'Error: ' + err.message;
|
|
2974
|
+
ruleStatusEl.className = 'rule-status error';
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
parseRulesBtn.disabled = false;
|
|
2978
|
+
parseRulesBtn.textContent = 'Parse Rules';
|
|
2979
|
+
});
|
|
2980
|
+
|
|
2981
|
+
// ============================================
|
|
2982
|
+
// SESSION TRACKING
|
|
2983
|
+
// ============================================
|
|
2984
|
+
|
|
2985
|
+
let sessionPollInterval = null;
|
|
2986
|
+
|
|
2987
|
+
async function pollSessionStats() {
|
|
2988
|
+
try {
|
|
2989
|
+
const resp = await fetch('/api/session');
|
|
2990
|
+
const data = await resp.json();
|
|
2991
|
+
const el = (id) => document.getElementById(id);
|
|
2992
|
+
if (el('s-total')) el('s-total').textContent = data.evaluations.total;
|
|
2993
|
+
if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
|
|
2994
|
+
if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
|
|
2995
|
+
if (el('s-allowed')) el('s-allowed').textContent = data.evaluations.allowed;
|
|
2996
|
+
if (el('s-agents')) {
|
|
2997
|
+
el('s-agents').textContent = data.agents.length > 0
|
|
2998
|
+
? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
|
|
2999
|
+
: 'No agents connected yet';
|
|
3000
|
+
}
|
|
3001
|
+
if (el('session-history') && data.historyCount > 0) {
|
|
3002
|
+
el('session-history').textContent = data.historyCount + ' previous session(s) saved for comparison';
|
|
3003
|
+
}
|
|
3004
|
+
} catch {}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
async function viewSessionReport() {
|
|
3008
|
+
try {
|
|
3009
|
+
const resp = await fetch('/api/session/report');
|
|
3010
|
+
const text = await resp.text();
|
|
3011
|
+
const log = document.getElementById('sim-log');
|
|
3012
|
+
if (log) {
|
|
3013
|
+
const div = document.createElement('div');
|
|
3014
|
+
div.className = 'log-round';
|
|
3015
|
+
div.innerHTML = '<h4 style="color:#818cf8">Enforcement Report</h4><pre style="white-space:pre-wrap;font-size:11px;color:#d4d4d8">' + text.replace(/</g,'<') + '</pre>';
|
|
3016
|
+
log.prepend(div);
|
|
3017
|
+
log.scrollTop = 0;
|
|
3018
|
+
}
|
|
3019
|
+
} catch (err) { console.error('Failed to load report', err); }
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
async function resetSession() {
|
|
3023
|
+
if (!confirm('Reset session? Current data will be saved for comparison.')) return;
|
|
3024
|
+
try {
|
|
3025
|
+
const resp = await fetch('/api/session/reset', { method: 'POST' });
|
|
3026
|
+
const data = await resp.json();
|
|
3027
|
+
const log = document.getElementById('sim-log');
|
|
3028
|
+
if (log) {
|
|
3029
|
+
const div = document.createElement('div');
|
|
3030
|
+
div.className = 'log-round';
|
|
3031
|
+
div.innerHTML = '<h4 style="color:#fbbf24">Session Reset</h4><div style="font-size:11px;color:#a8a29e">' + data.message + '</div>';
|
|
3032
|
+
log.prepend(div);
|
|
3033
|
+
}
|
|
3034
|
+
pollSessionStats();
|
|
3035
|
+
} catch (err) { console.error('Failed to reset session', err); }
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
async function saveExperiment() {
|
|
3039
|
+
try {
|
|
3040
|
+
const resp = await fetch('/api/session/save', { method: 'POST' });
|
|
3041
|
+
const data = await resp.json();
|
|
3042
|
+
if (data.error) { alert(data.error); return; }
|
|
3043
|
+
const log = document.getElementById('sim-log');
|
|
3044
|
+
if (log) {
|
|
3045
|
+
const div = document.createElement('div');
|
|
3046
|
+
div.className = 'log-round';
|
|
3047
|
+
div.innerHTML = '<h4 style="color:#4ade80">Experiment Saved</h4><div style="font-size:11px;color:#a8a29e">ID: ' + data.experimentId + '<br>File: ' + data.filePath + '<br>Stability: ' + (data.metrics.stability * 100).toFixed(0) + '% | Effectiveness: ' + (data.metrics.effectiveness * 100).toFixed(0) + '%</div>';
|
|
3048
|
+
log.prepend(div);
|
|
3049
|
+
}
|
|
3050
|
+
} catch (err) { console.error('Failed to save experiment', err); }
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// Poll session stats every 2 seconds
|
|
3054
|
+
sessionPollInterval = setInterval(pollSessionStats, 2000);
|
|
3055
|
+
pollSessionStats();
|
|
3056
|
+
|
|
3057
|
+
// ============================================
|
|
3058
|
+
// BOOT
|
|
3059
|
+
// ============================================
|
|
3060
|
+
init();
|
|
3061
|
+
<\/script>
|
|
3062
|
+
</body>
|
|
3063
|
+
</html>`;
|