@neuroverseos/nv-sim 0.1.4 → 0.1.7
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 +260 -6
- 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/{reportEngine-BfteK4MN.js → reportEngine-BVdQ2_nW.js} +1 -1
- 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 +427 -2
- package/dist/engine/auditTrace.js +352 -0
- package/dist/engine/behavioralAnalysis.js +605 -0
- package/dist/engine/cli.js +1087 -13
- package/dist/engine/dynamicsGovernance.js +588 -0
- package/dist/engine/fullGovernedLoop.js +367 -0
- package/dist/engine/governedSimulation.js +77 -6
- package/dist/engine/index.js +41 -1
- package/dist/engine/liveVisualizer.js +1961 -197
- package/dist/engine/metrics/science.metrics.js +335 -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/scenarioComparison.js +463 -0
- package/dist/engine/swarmSimulation.js +54 -1
- package/dist/engine/worldComparison.js +164 -0
- package/dist/engine/worldStorage.js +232 -0
- package/dist/index.html +2 -2
- 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/runtime/govern.js +473 -0
- package/dist/runtime/index.js +75 -0
- package/dist/runtime/types.js +11 -0
- package/package.json +5 -2
- package/dist/assets/index-DHKd4rcV.js +0 -338
- package/dist/assets/index-SyyA3z3U.css +0 -1
- package/dist/assets/swarmSimulation-DHDqjfMa.js +0 -1
|
@@ -48,6 +48,7 @@ const fs = __importStar(require("fs"));
|
|
|
48
48
|
const path = __importStar(require("path"));
|
|
49
49
|
const swarmSimulation_1 = require("./swarmSimulation");
|
|
50
50
|
const worldBridge_1 = require("./worldBridge");
|
|
51
|
+
const auditTrace_1 = require("./auditTrace");
|
|
51
52
|
const narrativeInjection_1 = require("./narrativeInjection");
|
|
52
53
|
const scenarioLibrary_1 = require("./scenarioLibrary");
|
|
53
54
|
const liveAdapter_1 = require("./liveAdapter");
|
|
@@ -97,9 +98,252 @@ function deleteVariant(variantId) {
|
|
|
97
98
|
* The UI sends POST /run-sim with world parameters.
|
|
98
99
|
* The server runs the simulation and streams results via SSE.
|
|
99
100
|
*/
|
|
101
|
+
/**
|
|
102
|
+
* Parse a single plain-English rule into a guard definition.
|
|
103
|
+
* No LLM needed — pattern matching on common financial governance phrases.
|
|
104
|
+
*/
|
|
105
|
+
function parseNaturalLanguageRule(line, index) {
|
|
106
|
+
const lower = line.toLowerCase().trim();
|
|
107
|
+
if (!lower)
|
|
108
|
+
return null;
|
|
109
|
+
// Determine enforcement action
|
|
110
|
+
let enforcement = "block";
|
|
111
|
+
if (/^(allow|permit|enable)\b/.test(lower))
|
|
112
|
+
enforcement = "allow";
|
|
113
|
+
else if (/^(pause|review|flag|hold|require.*review|require.*approval)\b/.test(lower))
|
|
114
|
+
enforcement = "pause";
|
|
115
|
+
else if (/^(block|ban|prevent|prohibit|stop|forbid|disallow|no\b|don.t allow)\b/.test(lower))
|
|
116
|
+
enforcement = "block";
|
|
117
|
+
else if (/^limit\b/.test(lower))
|
|
118
|
+
enforcement = "block";
|
|
119
|
+
// Extract intent patterns from the rule text
|
|
120
|
+
const patterns = [];
|
|
121
|
+
const actionWords = {
|
|
122
|
+
"panic sell": ["panic_sell", "panic sell", "panic selling"],
|
|
123
|
+
"panic buy": ["panic_buy", "panic buy", "panic buying"],
|
|
124
|
+
"short sell": ["short", "short_sell", "short selling", "shorting"],
|
|
125
|
+
"leverage": ["increase_leverage", "increase leverage", "max leverage", "excessive leverage"],
|
|
126
|
+
"large trade": ["large_trade", "large trade", "bulk trade", "big trade"],
|
|
127
|
+
"aggressive": ["aggressive_buy", "aggressive buy", "aggressive_short", "aggressive short"],
|
|
128
|
+
"flash": ["flash_trade", "flash trade", "high_frequency"],
|
|
129
|
+
"margin": ["margin_call", "margin call", "increase_margin"],
|
|
130
|
+
"liquidat": ["liquidate", "liquidation", "force_liquidate"],
|
|
131
|
+
"hedge": ["hedge", "hedging", "hedge_position"],
|
|
132
|
+
"buy": ["buy", "aggressive_buy"],
|
|
133
|
+
"sell": ["sell", "selling"],
|
|
134
|
+
"trade": ["trade", "trading"],
|
|
135
|
+
"withdraw": ["withdraw", "withdrawal", "bank_run"],
|
|
136
|
+
"circuit breaker": ["circuit_breaker", "halt_trading", "trading_halt"],
|
|
137
|
+
};
|
|
138
|
+
for (const [keyword, intents] of Object.entries(actionWords)) {
|
|
139
|
+
if (lower.includes(keyword)) {
|
|
140
|
+
patterns.push(...intents);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Fallback: extract quoted terms or the main noun phrase
|
|
144
|
+
if (patterns.length === 0) {
|
|
145
|
+
const quoted = line.match(/"([^"]+)"/g);
|
|
146
|
+
if (quoted) {
|
|
147
|
+
quoted.forEach(q => {
|
|
148
|
+
const term = q.replace(/"/g, "").trim();
|
|
149
|
+
patterns.push(term, term.replace(/\s+/g, "_"));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Extract the action part after block/allow/pause
|
|
154
|
+
const actionPart = lower.replace(/^(block|ban|prevent|prohibit|stop|forbid|disallow|allow|permit|enable|pause|review|flag|hold|limit|no|don.t allow)\s+/i, "").trim();
|
|
155
|
+
if (actionPart) {
|
|
156
|
+
patterns.push(actionPart, actionPart.replace(/\s+/g, "_"));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Deduplicate
|
|
161
|
+
const uniquePatterns = [...new Set(patterns)];
|
|
162
|
+
if (uniquePatterns.length === 0)
|
|
163
|
+
return null;
|
|
164
|
+
return {
|
|
165
|
+
id: `custom-rule-${index}`,
|
|
166
|
+
description: line,
|
|
167
|
+
enforcement,
|
|
168
|
+
intent_patterns: uniquePatterns,
|
|
169
|
+
category: "custom",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
100
172
|
function startInteractiveServer(port, onReady) {
|
|
101
173
|
const clients = new Set();
|
|
102
174
|
let isRunning = false;
|
|
175
|
+
// ── Persistent Audit Trail ──
|
|
176
|
+
// Every governance verdict is written to disk as JSONL
|
|
177
|
+
const auditTrail = new auditTrace_1.AuditTrail();
|
|
178
|
+
// Custom guards from plain-English rule editor (session-scoped)
|
|
179
|
+
const customGuards = [];
|
|
180
|
+
let currentSession = {
|
|
181
|
+
id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
182
|
+
startedAt: new Date().toISOString(),
|
|
183
|
+
world: "trading",
|
|
184
|
+
guardCount: 0,
|
|
185
|
+
evaluations: [],
|
|
186
|
+
};
|
|
187
|
+
// Session snapshots for multi-run comparison
|
|
188
|
+
const sessionHistory = [];
|
|
189
|
+
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
|
+
}
|
|
103
347
|
function broadcast(event) {
|
|
104
348
|
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
105
349
|
for (const client of clients) {
|
|
@@ -133,6 +377,18 @@ function startInteractiveServer(port, onReady) {
|
|
|
133
377
|
});
|
|
134
378
|
world.state_variables = updatedVars;
|
|
135
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
|
+
}
|
|
136
392
|
// Resolve narrative events
|
|
137
393
|
let narrativeEvents = [];
|
|
138
394
|
if (config.scenarioId && scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]) {
|
|
@@ -156,7 +412,12 @@ function startInteractiveServer(port, onReady) {
|
|
|
156
412
|
scenario: resolved.scenario,
|
|
157
413
|
worldThesis: world.thesis,
|
|
158
414
|
agents: resolved.stakeholders.map(s => s.id),
|
|
159
|
-
invariants: world.invariants.map(inv => ({
|
|
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
|
+
})),
|
|
160
421
|
gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
|
|
161
422
|
narrativeEvents: narrativeEvents.map(e => ({ id: e.id, headline: e.headline, round: e.round, severity: e.severity })),
|
|
162
423
|
totalRounds: rounds,
|
|
@@ -372,47 +633,170 @@ function startInteractiveServer(port, onReady) {
|
|
|
372
633
|
// Without guards, everything defaults to ALLOW. These guards define what
|
|
373
634
|
// simulation actions should be governed.
|
|
374
635
|
if (!nvWorld.guards) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
description: "Prevents increasing leverage positions that could amplify cascades",
|
|
392
|
-
category: "structural",
|
|
393
|
-
enforcement: "block",
|
|
394
|
-
immutable: true,
|
|
395
|
-
intent_patterns: ["increase_leverage", "increase leverage", "max leverage"],
|
|
396
|
-
default_enabled: true,
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
id: "sim-aggressive-actions",
|
|
400
|
-
label: "Pause aggressive market actions",
|
|
401
|
-
description: "Requires review for aggressive buying or shorting that could move markets",
|
|
402
|
-
category: "operational",
|
|
403
|
-
enforcement: "pause",
|
|
404
|
-
immutable: false,
|
|
405
|
-
intent_patterns: ["aggressive_buy", "aggressive buy", "aggressive_short", "short"],
|
|
406
|
-
default_enabled: true,
|
|
407
|
-
},
|
|
408
|
-
],
|
|
409
|
-
intent_vocabulary: {
|
|
410
|
-
"panic_sell": { label: "Panic Sell", pattern: "panic_sell" },
|
|
411
|
-
"increase_leverage": { label: "Increase Leverage", pattern: "increase_leverage" },
|
|
412
|
-
"aggressive_buy": { label: "Aggressive Buy", pattern: "aggressive_buy" },
|
|
413
|
-
"short": { label: "Short Position", pattern: "short" },
|
|
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,
|
|
414
652
|
},
|
|
415
|
-
|
|
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;
|
|
416
800
|
}
|
|
417
801
|
// Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
|
|
418
802
|
// Omit `direction` — setting it enables execution-intent safety checks (prompt injection
|
|
@@ -423,7 +807,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
423
807
|
tool: "simulation",
|
|
424
808
|
scope: `bridge/${payload.actor}`,
|
|
425
809
|
actionCategory: "execute",
|
|
426
|
-
riskLevel: (["panic_sell", "panic_buy", "increase_leverage"].includes(payload.action) ? "high" : "medium"),
|
|
810
|
+
riskLevel: (["panic_sell", "panic_buy", "increase_leverage", "create_post", "repost", "quote_post"].includes(payload.action) ? "high" : "medium"),
|
|
427
811
|
args: {
|
|
428
812
|
actor: payload.actor,
|
|
429
813
|
action: payload.action,
|
|
@@ -469,6 +853,31 @@ function startInteractiveServer(port, onReady) {
|
|
|
469
853
|
dynamics: [],
|
|
470
854
|
interventionCount: decision !== "ALLOW" ? 1 : 0,
|
|
471
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
|
+
});
|
|
472
881
|
jsonResponse(res, 200, {
|
|
473
882
|
decision,
|
|
474
883
|
reason: verdict.reason ?? null,
|
|
@@ -478,6 +887,12 @@ function startInteractiveServer(port, onReady) {
|
|
|
478
887
|
});
|
|
479
888
|
}
|
|
480
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
|
+
});
|
|
481
896
|
// Fail open — return ALLOW on any error
|
|
482
897
|
jsonResponse(res, 200, {
|
|
483
898
|
decision: "ALLOW",
|
|
@@ -489,6 +904,395 @@ function startInteractiveServer(port, onReady) {
|
|
|
489
904
|
}
|
|
490
905
|
return;
|
|
491
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
|
+
// ── Clear Rules ──
|
|
962
|
+
// Wipes all custom guards and resets governance to base world rules
|
|
963
|
+
if (req.url === "/api/clear-rules" && req.method === "POST") {
|
|
964
|
+
customGuards.length = 0;
|
|
965
|
+
jsonResponse(res, 200, {
|
|
966
|
+
status: "cleared",
|
|
967
|
+
message: "All custom rules removed. Governance reset to base world rules.",
|
|
968
|
+
});
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
// ── Load World File ──
|
|
972
|
+
// Accept a full world definition JSON and use it as the active world
|
|
973
|
+
if (req.url === "/api/load-world-file" && req.method === "POST") {
|
|
974
|
+
try {
|
|
975
|
+
const body = await readBody(req);
|
|
976
|
+
const payload = JSON.parse(body);
|
|
977
|
+
if (!payload.world) {
|
|
978
|
+
jsonResponse(res, 400, { error: "world object is required" });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const w = payload.world;
|
|
982
|
+
// Parse any plain-English rules in the world file into guards
|
|
983
|
+
customGuards.length = 0;
|
|
984
|
+
if (w.rules && Array.isArray(w.rules)) {
|
|
985
|
+
for (let i = 0; i < w.rules.length; i++) {
|
|
986
|
+
const rule = w.rules[i];
|
|
987
|
+
if (rule.intent_patterns && rule.intent_patterns.length > 0) {
|
|
988
|
+
// Already structured rule
|
|
989
|
+
customGuards.push({
|
|
990
|
+
id: rule.id || `world-rule-${i}`,
|
|
991
|
+
label: rule.description,
|
|
992
|
+
description: rule.description,
|
|
993
|
+
category: "world-file",
|
|
994
|
+
enforcement: rule.enforcement || "block",
|
|
995
|
+
immutable: false,
|
|
996
|
+
intent_patterns: rule.intent_patterns,
|
|
997
|
+
default_enabled: true,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
// Plain-English rule — parse it
|
|
1002
|
+
const parsed = parseNaturalLanguageRule(rule.description, i);
|
|
1003
|
+
if (parsed) {
|
|
1004
|
+
customGuards.push({
|
|
1005
|
+
id: parsed.id,
|
|
1006
|
+
label: parsed.description,
|
|
1007
|
+
description: parsed.description,
|
|
1008
|
+
category: "world-file",
|
|
1009
|
+
enforcement: parsed.enforcement,
|
|
1010
|
+
immutable: false,
|
|
1011
|
+
intent_patterns: parsed.intent_patterns,
|
|
1012
|
+
default_enabled: true,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Build the response world definition for the UI
|
|
1019
|
+
const loadedWorld = {
|
|
1020
|
+
id: "custom-world",
|
|
1021
|
+
title: w.name || "Custom World",
|
|
1022
|
+
thesis: w.thesis || "User-defined world",
|
|
1023
|
+
stateVariables: w.state_variables || [],
|
|
1024
|
+
invariants: (w.invariants || []).map(inv => ({
|
|
1025
|
+
id: inv.id,
|
|
1026
|
+
description: inv.description,
|
|
1027
|
+
enforceable: inv.enforceable !== false,
|
|
1028
|
+
})),
|
|
1029
|
+
gates: (w.gates || []).map(g => ({
|
|
1030
|
+
id: g.id,
|
|
1031
|
+
label: g.label,
|
|
1032
|
+
condition: g.condition,
|
|
1033
|
+
severity: g.severity || "warning",
|
|
1034
|
+
})),
|
|
1035
|
+
rulesApplied: customGuards.length,
|
|
1036
|
+
};
|
|
1037
|
+
jsonResponse(res, 200, {
|
|
1038
|
+
status: "loaded",
|
|
1039
|
+
world: loadedWorld,
|
|
1040
|
+
rulesApplied: customGuards.length,
|
|
1041
|
+
message: `World "${loadedWorld.title}" loaded with ${loadedWorld.invariants.length} invariants, ${loadedWorld.gates.length} gates, and ${customGuards.length} rules.`,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
catch (err) {
|
|
1045
|
+
jsonResponse(res, 400, { error: "Invalid world file JSON" });
|
|
1046
|
+
}
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
// ── Export World File ──
|
|
1050
|
+
// Exports the current world configuration (base world + custom rules + overrides) as a world file
|
|
1051
|
+
if (req.url === "/api/export-world" && req.method === "GET") {
|
|
1052
|
+
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
1053
|
+
const worldId = currentSession.world || "trading";
|
|
1054
|
+
let baseWorld;
|
|
1055
|
+
try {
|
|
1056
|
+
baseWorld = resolveWorld(worldId);
|
|
1057
|
+
}
|
|
1058
|
+
catch {
|
|
1059
|
+
baseWorld = null;
|
|
1060
|
+
}
|
|
1061
|
+
const exportedWorld = {
|
|
1062
|
+
name: baseWorld?.title || worldId,
|
|
1063
|
+
thesis: baseWorld?.world?.thesis || "Exported world",
|
|
1064
|
+
state_variables: baseWorld?.world?.state_variables || [],
|
|
1065
|
+
invariants: baseWorld?.world?.invariants || [],
|
|
1066
|
+
gates: baseWorld?.world?.gates || [],
|
|
1067
|
+
rules: customGuards.map(g => ({
|
|
1068
|
+
id: g.id,
|
|
1069
|
+
description: g.description,
|
|
1070
|
+
enforcement: g.enforcement,
|
|
1071
|
+
intent_patterns: g.intent_patterns,
|
|
1072
|
+
})),
|
|
1073
|
+
};
|
|
1074
|
+
res.writeHead(200, {
|
|
1075
|
+
"Content-Type": "application/json",
|
|
1076
|
+
"Content-Disposition": `attachment; filename="${worldId}-world.json"`,
|
|
1077
|
+
"Access-Control-Allow-Origin": "*",
|
|
1078
|
+
});
|
|
1079
|
+
res.end(JSON.stringify({ world: exportedWorld }, null, 2));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
// ── Session Reporting Endpoints ──
|
|
1083
|
+
// Connect the serve runtime to the enforce reporting pipeline.
|
|
1084
|
+
// Users can request reports, stats, and recommendations from live governance data.
|
|
1085
|
+
// GET /api/session — current session stats (lightweight)
|
|
1086
|
+
if (req.url === "/api/session" && req.method === "GET") {
|
|
1087
|
+
const evals = currentSession.evaluations;
|
|
1088
|
+
const blocked = evals.filter(e => e.decision === "BLOCK").length;
|
|
1089
|
+
const modified = evals.filter(e => e.decision === "MODIFY").length;
|
|
1090
|
+
const allowed = evals.filter(e => e.decision === "ALLOW").length;
|
|
1091
|
+
const uniqueActors = [...new Set(evals.map(e => e.actor))];
|
|
1092
|
+
const uniqueActions = [...new Set(evals.map(e => e.action))];
|
|
1093
|
+
const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
|
|
1094
|
+
jsonResponse(res, 200, {
|
|
1095
|
+
sessionId: currentSession.id,
|
|
1096
|
+
startedAt: currentSession.startedAt,
|
|
1097
|
+
world: currentSession.world,
|
|
1098
|
+
guardCount: currentSession.guardCount,
|
|
1099
|
+
evaluations: {
|
|
1100
|
+
total: evals.length,
|
|
1101
|
+
blocked,
|
|
1102
|
+
modified,
|
|
1103
|
+
allowed,
|
|
1104
|
+
},
|
|
1105
|
+
agents: uniqueActors,
|
|
1106
|
+
actionTypes: uniqueActions,
|
|
1107
|
+
triggeredRules,
|
|
1108
|
+
historyCount: sessionHistory.length,
|
|
1109
|
+
});
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
// GET /api/session/report — full enforcement report (text)
|
|
1113
|
+
if (req.url === "/api/session/report" && req.method === "GET") {
|
|
1114
|
+
const report = synthesizeSessionReport();
|
|
1115
|
+
// Format as human-readable text using the same style as enforce CLI
|
|
1116
|
+
const lines = [];
|
|
1117
|
+
lines.push("");
|
|
1118
|
+
lines.push(" LIVE GOVERNANCE — ENFORCEMENT REPORT");
|
|
1119
|
+
lines.push(" " + "=".repeat(70));
|
|
1120
|
+
lines.push(` Session: ${report.sessionId}`);
|
|
1121
|
+
lines.push(` Scenario: ${report.scenario}`);
|
|
1122
|
+
lines.push(` Sessions: ${report.runs.length}`);
|
|
1123
|
+
lines.push(` Generated: ${report.generatedAt}`);
|
|
1124
|
+
lines.push("");
|
|
1125
|
+
if (report.runs.length > 0) {
|
|
1126
|
+
lines.push(" RUN HISTORY");
|
|
1127
|
+
lines.push(" " + "-".repeat(70));
|
|
1128
|
+
lines.push(` ${"#".padEnd(4)} ${"World".padEnd(25)} ${"Rules".padEnd(8)} ${"Evals".padEnd(8)} ${"Blocked".padEnd(10)} ${"Effectiveness"}`);
|
|
1129
|
+
lines.push(" " + "-".repeat(70));
|
|
1130
|
+
for (const run of report.runs) {
|
|
1131
|
+
const name = run.worldName.length > 23 ? run.worldName.slice(0, 22) + "…" : run.worldName;
|
|
1132
|
+
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)}%`);
|
|
1133
|
+
}
|
|
1134
|
+
lines.push("");
|
|
1135
|
+
lines.push(" DIVERGENCE ANALYSIS");
|
|
1136
|
+
lines.push(" " + "-".repeat(70));
|
|
1137
|
+
lines.push(` Stability trend: ${report.divergence.stabilityTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1138
|
+
lines.push(` Collapse trend: ${report.divergence.collapseTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1139
|
+
lines.push(` Effectiveness trend: ${report.divergence.effectivenessTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1140
|
+
lines.push(` Total divergence: ${report.divergence.totalDivergence}`);
|
|
1141
|
+
lines.push("");
|
|
1142
|
+
lines.push(` ${report.divergence.narrative}`);
|
|
1143
|
+
// Blocked action breakdown
|
|
1144
|
+
const allEvals = [
|
|
1145
|
+
...sessionHistory.flatMap(s => s.evaluations),
|
|
1146
|
+
...currentSession.evaluations,
|
|
1147
|
+
];
|
|
1148
|
+
const blockedEvals = allEvals.filter(e => e.decision === "BLOCK");
|
|
1149
|
+
if (blockedEvals.length > 0) {
|
|
1150
|
+
lines.push("");
|
|
1151
|
+
lines.push(" BLOCKED ACTIONS");
|
|
1152
|
+
lines.push(" " + "-".repeat(70));
|
|
1153
|
+
// Group by rule
|
|
1154
|
+
const byRule = {};
|
|
1155
|
+
for (const e of blockedEvals) {
|
|
1156
|
+
const key = e.ruleId ?? "unknown";
|
|
1157
|
+
if (!byRule[key])
|
|
1158
|
+
byRule[key] = { count: 0, actors: new Set(), actions: new Set() };
|
|
1159
|
+
byRule[key].count++;
|
|
1160
|
+
byRule[key].actors.add(e.actor);
|
|
1161
|
+
byRule[key].actions.add(e.action);
|
|
1162
|
+
}
|
|
1163
|
+
const sorted = Object.entries(byRule).sort((a, b) => b[1].count - a[1].count);
|
|
1164
|
+
for (const [ruleId, data] of sorted) {
|
|
1165
|
+
lines.push(` ${ruleId}: ${data.count} blocks across ${data.actors.size} agent(s)`);
|
|
1166
|
+
lines.push(` Actions: ${[...data.actions].join(", ")}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
// Recommendations (deterministic, no LLM needed)
|
|
1170
|
+
lines.push("");
|
|
1171
|
+
lines.push(" RECOMMENDATIONS");
|
|
1172
|
+
lines.push(" " + "-".repeat(70));
|
|
1173
|
+
const best = report.runs[report.divergence.bestIteration - 1];
|
|
1174
|
+
if (best) {
|
|
1175
|
+
const totalEvals = best.governanceStats.totalEvaluations;
|
|
1176
|
+
const blockRate = totalEvals > 0 ? best.governanceStats.verdicts.block / totalEvals : 0;
|
|
1177
|
+
if (blockRate > 0.5) {
|
|
1178
|
+
lines.push(" HIGH BLOCK RATE — Rules may be too aggressive.");
|
|
1179
|
+
lines.push(" Try relaxing constraints or adding MODIFY actions instead of hard blocks.");
|
|
1180
|
+
lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
|
|
1181
|
+
}
|
|
1182
|
+
else if (blockRate < 0.05 && totalEvals > 20) {
|
|
1183
|
+
lines.push(" LOW BLOCK RATE — Rules may be too permissive.");
|
|
1184
|
+
lines.push(" Try adding content-specific guards or lowering thresholds.");
|
|
1185
|
+
lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
|
|
1186
|
+
}
|
|
1187
|
+
else if (blockRate > 0) {
|
|
1188
|
+
lines.push(` Governance is active: ${(blockRate * 100).toFixed(0)}% of actions blocked.`);
|
|
1189
|
+
lines.push(" To iterate: apply different rules via POST /api/apply-rules, then POST /api/session/reset.");
|
|
1190
|
+
lines.push(" This creates a new session for side-by-side comparison.");
|
|
1191
|
+
}
|
|
1192
|
+
if (report.runs.length === 1) {
|
|
1193
|
+
lines.push("");
|
|
1194
|
+
lines.push(" EXPERIMENT: Apply different rules and reset to compare sessions.");
|
|
1195
|
+
lines.push(" This will show divergence: how different governance changes outcomes.");
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
lines.push("");
|
|
1199
|
+
lines.push(" RECOMMENDATION");
|
|
1200
|
+
lines.push(" " + "-".repeat(70));
|
|
1201
|
+
lines.push(` ${report.recommendation}`);
|
|
1202
|
+
}
|
|
1203
|
+
lines.push("");
|
|
1204
|
+
lines.push(" " + "=".repeat(70));
|
|
1205
|
+
lines.push(" NeuroVerse Policy Enforcement System — Live Governance");
|
|
1206
|
+
lines.push(" Design rules. Run reality. See what changes.");
|
|
1207
|
+
lines.push(" " + "=".repeat(70));
|
|
1208
|
+
lines.push("");
|
|
1209
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
1210
|
+
res.end(lines.join("\n"));
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
// GET /api/session/report.json — full enforcement report (JSON)
|
|
1214
|
+
if (req.url === "/api/session/report.json" && req.method === "GET") {
|
|
1215
|
+
const report = synthesizeSessionReport();
|
|
1216
|
+
jsonResponse(res, 200, report);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
// GET /api/session/evaluations — raw evaluation log
|
|
1220
|
+
if (req.url === "/api/session/evaluations" && req.method === "GET") {
|
|
1221
|
+
jsonResponse(res, 200, {
|
|
1222
|
+
sessionId: currentSession.id,
|
|
1223
|
+
count: currentSession.evaluations.length,
|
|
1224
|
+
evaluations: currentSession.evaluations.slice(-200), // last 200
|
|
1225
|
+
});
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
// POST /api/session/reset — snapshot current session and start fresh
|
|
1229
|
+
// This is how users create multi-run comparisons:
|
|
1230
|
+
// 1. Apply rules A → run simulation → POST /api/session/reset
|
|
1231
|
+
// 2. Apply rules B → run simulation → GET /api/session/report
|
|
1232
|
+
// Now the report shows divergence between rules A and rules B.
|
|
1233
|
+
if (req.url === "/api/session/reset" && req.method === "POST") {
|
|
1234
|
+
if (currentSession.evaluations.length > 0) {
|
|
1235
|
+
sessionHistory.push({ ...currentSession, evaluations: [...currentSession.evaluations] });
|
|
1236
|
+
}
|
|
1237
|
+
const body = await readBody(req).catch(() => "{}");
|
|
1238
|
+
const opts = JSON.parse(body || "{}");
|
|
1239
|
+
currentSession = {
|
|
1240
|
+
id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1241
|
+
startedAt: new Date().toISOString(),
|
|
1242
|
+
world: opts.world ?? currentSession.world,
|
|
1243
|
+
guardCount: customGuards.length,
|
|
1244
|
+
evaluations: [],
|
|
1245
|
+
};
|
|
1246
|
+
jsonResponse(res, 200, {
|
|
1247
|
+
status: "reset",
|
|
1248
|
+
newSessionId: currentSession.id,
|
|
1249
|
+
previousSessions: sessionHistory.length,
|
|
1250
|
+
message: `Session reset. ${sessionHistory.length} session(s) in history. Apply new rules and evaluate to compare.`,
|
|
1251
|
+
});
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
// POST /api/session/save — save session as experiment with lineage
|
|
1255
|
+
if (req.url === "/api/session/save" && req.method === "POST") {
|
|
1256
|
+
const report = synthesizeSessionReport();
|
|
1257
|
+
if (report.runs.length === 0) {
|
|
1258
|
+
jsonResponse(res, 400, { error: "No evaluations to save. Send actions to /api/evaluate first." });
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const experiment = {
|
|
1262
|
+
id: `exp-live-${Date.now().toString(36)}`,
|
|
1263
|
+
savedAt: new Date().toISOString(),
|
|
1264
|
+
scenario: report.scenario,
|
|
1265
|
+
source: "live-governance",
|
|
1266
|
+
sessions: report.runs.length,
|
|
1267
|
+
totalEvaluations: report.runs.reduce((sum, r) => sum + r.governanceStats.totalEvaluations, 0),
|
|
1268
|
+
metrics: {
|
|
1269
|
+
stability: report.runs[report.divergence.bestIteration - 1]?.metrics.stabilityScore ?? 0,
|
|
1270
|
+
effectiveness: report.runs[report.divergence.bestIteration - 1]?.comparison.governanceEffectiveness ?? 0,
|
|
1271
|
+
collapseProbability: report.runs[report.divergence.bestIteration - 1]?.metrics.collapseProbability ?? 0,
|
|
1272
|
+
},
|
|
1273
|
+
divergence: report.divergence,
|
|
1274
|
+
recommendation: report.recommendation,
|
|
1275
|
+
report,
|
|
1276
|
+
};
|
|
1277
|
+
// Save to experiments/ directory
|
|
1278
|
+
try {
|
|
1279
|
+
if (!fs.existsSync("experiments"))
|
|
1280
|
+
fs.mkdirSync("experiments", { recursive: true });
|
|
1281
|
+
const filePath = path.join("experiments", `${experiment.id}.json`);
|
|
1282
|
+
fs.writeFileSync(filePath, JSON.stringify(experiment, null, 2));
|
|
1283
|
+
jsonResponse(res, 200, {
|
|
1284
|
+
status: "saved",
|
|
1285
|
+
experimentId: experiment.id,
|
|
1286
|
+
filePath,
|
|
1287
|
+
metrics: experiment.metrics,
|
|
1288
|
+
message: `Saved to ${filePath}. View with: npx nv-sim enforce --load ${filePath}`,
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
catch (err) {
|
|
1292
|
+
jsonResponse(res, 500, { error: "Failed to save experiment", detail: String(err) });
|
|
1293
|
+
}
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
492
1296
|
// List available live adapters
|
|
493
1297
|
if (req.url === "/api/adapters" && req.method === "GET") {
|
|
494
1298
|
const adapters = Object.values(liveAdapter_1.ADAPTER_REGISTRY).map(a => ({
|
|
@@ -616,6 +1420,81 @@ function startInteractiveServer(port, onReady) {
|
|
|
616
1420
|
}
|
|
617
1421
|
return;
|
|
618
1422
|
}
|
|
1423
|
+
// ── AUDIT TRAIL ENDPOINTS ──
|
|
1424
|
+
// GET /api/audit — summary of current session's audit trail
|
|
1425
|
+
if (req.url === "/api/audit" && req.method === "GET") {
|
|
1426
|
+
jsonResponse(res, 200, auditTrail.summary());
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
// GET /api/audit/entries — all entries from current session (with optional filters)
|
|
1430
|
+
if (req.url?.startsWith("/api/audit/entries") && req.method === "GET") {
|
|
1431
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1432
|
+
const entries = auditTrail.query({
|
|
1433
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1434
|
+
agent: url.searchParams.get("agent") ?? undefined,
|
|
1435
|
+
verdict: url.searchParams.get("verdict") ?? undefined,
|
|
1436
|
+
runId: url.searchParams.get("runId") ?? undefined,
|
|
1437
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
1438
|
+
before: url.searchParams.get("before") ?? undefined,
|
|
1439
|
+
});
|
|
1440
|
+
jsonResponse(res, 200, {
|
|
1441
|
+
sessionId: auditTrail.getSessionId(),
|
|
1442
|
+
count: entries.length,
|
|
1443
|
+
entries,
|
|
1444
|
+
});
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
// GET /api/audit/entries/text — human-readable audit trail
|
|
1448
|
+
if (req.url?.startsWith("/api/audit/entries/text") && req.method === "GET") {
|
|
1449
|
+
const entries = auditTrail.readAll();
|
|
1450
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
1451
|
+
res.end((0, auditTrace_1.formatAuditTrail)(entries, { verbose: true }));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
// GET /api/audit/sessions — list all audit sessions on disk
|
|
1455
|
+
if (req.url === "/api/audit/sessions" && req.method === "GET") {
|
|
1456
|
+
const sessions = (0, auditTrace_1.listAuditSessions)();
|
|
1457
|
+
jsonResponse(res, 200, {
|
|
1458
|
+
sessions: sessions.map((id) => {
|
|
1459
|
+
const trail = (0, auditTrace_1.loadAuditSession)(id);
|
|
1460
|
+
return trail.summary();
|
|
1461
|
+
}),
|
|
1462
|
+
});
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
// GET /api/audit/sessions/:id — load a specific session's audit trail
|
|
1466
|
+
const auditSessionMatch = req.url?.match(/^\/api\/audit\/sessions\/([^/]+)$/);
|
|
1467
|
+
if (auditSessionMatch && req.method === "GET") {
|
|
1468
|
+
const sessionId = decodeURIComponent(auditSessionMatch[1]);
|
|
1469
|
+
const trail = (0, auditTrace_1.loadAuditSession)(sessionId);
|
|
1470
|
+
const entries = trail.readAll();
|
|
1471
|
+
if (entries.length === 0) {
|
|
1472
|
+
jsonResponse(res, 404, { error: `Audit session "${sessionId}" not found or empty` });
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
jsonResponse(res, 200, {
|
|
1476
|
+
...trail.summary(),
|
|
1477
|
+
entries,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
// GET /api/audit/search — search across ALL sessions
|
|
1483
|
+
if (req.url?.startsWith("/api/audit/search") && req.method === "GET") {
|
|
1484
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1485
|
+
const results = (0, auditTrace_1.searchAuditTrails)({
|
|
1486
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1487
|
+
agent: url.searchParams.get("agent") ?? undefined,
|
|
1488
|
+
verdict: url.searchParams.get("verdict") ?? undefined,
|
|
1489
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
1490
|
+
before: url.searchParams.get("before") ?? undefined,
|
|
1491
|
+
});
|
|
1492
|
+
jsonResponse(res, 200, {
|
|
1493
|
+
count: results.length,
|
|
1494
|
+
entries: results,
|
|
1495
|
+
});
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
619
1498
|
// Serve the interactive dashboard
|
|
620
1499
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
621
1500
|
res.end(INTERACTIVE_DASHBOARD_HTML);
|
|
@@ -632,251 +1511,519 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
632
1511
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
633
1512
|
<title>NV-SIM — Scenario Control Platform</title>
|
|
634
1513
|
<style>
|
|
1514
|
+
/* ── Theme Variables ── */
|
|
1515
|
+
:root {
|
|
1516
|
+
--bg-primary: #0f0f0f;
|
|
1517
|
+
--bg-secondary: #141414;
|
|
1518
|
+
--bg-surface: #1a1a1a;
|
|
1519
|
+
--bg-elevated: #222;
|
|
1520
|
+
--border: #2a2a2a;
|
|
1521
|
+
--border-subtle: #333;
|
|
1522
|
+
--text-primary: #f0f0f0;
|
|
1523
|
+
--text-secondary: #b0b0b0;
|
|
1524
|
+
--text-muted: #888;
|
|
1525
|
+
--text-faint: #666;
|
|
1526
|
+
--accent: #818cf8;
|
|
1527
|
+
--accent-bg: #1e1e3a;
|
|
1528
|
+
--green: #4ade80;
|
|
1529
|
+
--green-bg: #0a2a14;
|
|
1530
|
+
--red: #f87171;
|
|
1531
|
+
--red-bg: #2d0a0a;
|
|
1532
|
+
--yellow: #fbbf24;
|
|
1533
|
+
--yellow-bg: #2d2006;
|
|
1534
|
+
--blue: #60a5fa;
|
|
1535
|
+
--blue-bg: #1e293b;
|
|
1536
|
+
--purple: #a78bfa;
|
|
1537
|
+
}
|
|
1538
|
+
body.light {
|
|
1539
|
+
--bg-primary: #f5f5f5;
|
|
1540
|
+
--bg-secondary: #eaeaea;
|
|
1541
|
+
--bg-surface: #e0e0e0;
|
|
1542
|
+
--bg-elevated: #d4d4d4;
|
|
1543
|
+
--border: #c0c0c0;
|
|
1544
|
+
--border-subtle: #b0b0b0;
|
|
1545
|
+
--text-primary: #1a1a1a;
|
|
1546
|
+
--text-secondary: #444;
|
|
1547
|
+
--text-muted: #666;
|
|
1548
|
+
--text-faint: #888;
|
|
1549
|
+
--accent: #6366f1;
|
|
1550
|
+
--accent-bg: #e8e8ff;
|
|
1551
|
+
--green: #16a34a;
|
|
1552
|
+
--green-bg: #dcfce7;
|
|
1553
|
+
--red: #dc2626;
|
|
1554
|
+
--red-bg: #fee2e2;
|
|
1555
|
+
--yellow: #ca8a04;
|
|
1556
|
+
--yellow-bg: #fef9c3;
|
|
1557
|
+
--blue: #2563eb;
|
|
1558
|
+
--blue-bg: #dbeafe;
|
|
1559
|
+
--purple: #7c3aed;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
635
1562
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
636
|
-
body { font-family: 'SF Mono', 'Fira Code', monospace; background:
|
|
637
|
-
.header { padding: 12px 20px; border-bottom: 1px solid
|
|
638
|
-
.header h1 { font-size: 15px; color:
|
|
639
|
-
.header .sub { font-size: 11px; color:
|
|
1563
|
+
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; }
|
|
1564
|
+
.header { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
1565
|
+
.header h1 { font-size: 15px; color: var(--text-primary); }
|
|
1566
|
+
.header .sub { font-size: 11px; color: var(--text-muted); margin-left: 12px; }
|
|
1567
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
1568
|
+
.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; }
|
|
1569
|
+
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
|
640
1570
|
.status { font-size: 11px; padding: 3px 10px; border-radius: 10px; }
|
|
641
|
-
.status.idle { background:
|
|
642
|
-
.status.live { background:
|
|
643
|
-
.status.complete { background:
|
|
1571
|
+
.status.idle { background: var(--accent-bg); color: var(--accent); }
|
|
1572
|
+
.status.live { background: var(--green-bg); color: var(--green); animation: pulse 2s infinite; }
|
|
1573
|
+
.status.complete { background: var(--accent-bg); color: var(--accent); }
|
|
644
1574
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
645
1575
|
|
|
646
1576
|
.layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }
|
|
647
1577
|
|
|
648
1578
|
/* LEFT PANEL — Controls */
|
|
649
|
-
.controls { background:
|
|
1579
|
+
.controls { background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px; }
|
|
650
1580
|
.ctrl-section { margin-bottom: 20px; }
|
|
651
|
-
.ctrl-section h3 { font-size: 11px; color:
|
|
1581
|
+
.ctrl-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
|
|
652
1582
|
|
|
653
1583
|
.ctrl-row { margin-bottom: 12px; }
|
|
654
|
-
.ctrl-label { font-size: 11px; color:
|
|
655
|
-
.ctrl-label .val { color:
|
|
1584
|
+
.ctrl-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; display: flex; justify-content: space-between; }
|
|
1585
|
+
.ctrl-label .val { color: var(--text-primary); font-weight: 600; }
|
|
656
1586
|
|
|
657
|
-
input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background:
|
|
658
|
-
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background:
|
|
659
|
-
input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background:
|
|
1587
|
+
input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: var(--bg-elevated); border-radius: 2px; outline: none; }
|
|
1588
|
+
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; }
|
|
1589
|
+
input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: none; }
|
|
660
1590
|
|
|
661
|
-
select { width: 100%; background:
|
|
1591
|
+
select { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
|
|
662
1592
|
|
|
663
1593
|
.toggle-row { display: flex; align-items: center; gap: 8px; }
|
|
664
|
-
.toggle { position: relative; width: 36px; height: 20px; background:
|
|
665
|
-
.toggle.on { background:
|
|
1594
|
+
.toggle { position: relative; width: 36px; height: 20px; background: var(--border-subtle); border-radius: 10px; cursor: pointer; transition: background 0.2s; }
|
|
1595
|
+
.toggle.on { background: var(--green); }
|
|
666
1596
|
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
|
|
667
1597
|
.toggle.on::after { transform: translateX(16px); }
|
|
668
|
-
.toggle-label { font-size: 12px; color:
|
|
1598
|
+
.toggle-label { font-size: 12px; color: var(--text-secondary); }
|
|
669
1599
|
|
|
670
1600
|
.inject-row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
671
1601
|
.inject-row select { flex: 1; }
|
|
672
|
-
.inject-row input { width: 50px; background:
|
|
1602
|
+
.inject-row input { width: 50px; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 4px 6px; border-radius: 4px; font-family: inherit; font-size: 12px; text-align: center; }
|
|
673
1603
|
.inject-list { margin-bottom: 8px; }
|
|
674
|
-
.inject-item { font-size: 11px; color:
|
|
675
|
-
.inject-item .remove { color:
|
|
1604
|
+
.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; }
|
|
1605
|
+
.inject-item .remove { color: var(--red); cursor: pointer; }
|
|
676
1606
|
|
|
677
1607
|
.btn { width: 100%; padding: 10px; border: none; border-radius: 6px; font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s; }
|
|
678
|
-
.btn-run { background:
|
|
1608
|
+
.btn-run { background: var(--green); color: #0a0a0a; }
|
|
679
1609
|
.btn-run:hover { background: #22c55e; }
|
|
680
|
-
.btn-run:disabled { background:
|
|
681
|
-
.btn-add { background:
|
|
682
|
-
.btn-add:hover { background:
|
|
1610
|
+
.btn-run:disabled { background: var(--bg-elevated); color: var(--text-faint); cursor: not-allowed; }
|
|
1611
|
+
.btn-add { background: var(--bg-elevated); color: var(--accent); padding: 6px; font-size: 11px; }
|
|
1612
|
+
.btn-add:hover { background: var(--accent-bg); }
|
|
683
1613
|
|
|
684
|
-
.scenario-btn { display: block; width: 100%; padding: 8px 10px; margin-bottom: 4px; background:
|
|
685
|
-
.scenario-btn:hover { border-color:
|
|
686
|
-
.scenario-btn .stitle { font-weight: 600; color:
|
|
687
|
-
.scenario-btn .sdesc { color:
|
|
1614
|
+
.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; }
|
|
1615
|
+
.scenario-btn:hover { border-color: var(--accent); background: var(--accent-bg); }
|
|
1616
|
+
.scenario-btn .stitle { font-weight: 600; color: var(--text-primary); }
|
|
1617
|
+
.scenario-btn .sdesc { color: var(--text-muted); font-size: 10px; margin-top: 2px; }
|
|
688
1618
|
|
|
689
1619
|
/* Save variant */
|
|
690
|
-
.btn-save { background:
|
|
691
|
-
.btn-save:hover { background:
|
|
692
|
-
.btn-confirm { flex: 1; background:
|
|
693
|
-
.btn-cancel { flex: 1; background:
|
|
694
|
-
.save-input { width: 100%; background:
|
|
695
|
-
.save-input:focus { border-color:
|
|
1620
|
+
.btn-save { background: var(--accent-bg); color: var(--accent); margin-top: 0; }
|
|
1621
|
+
.btn-save:hover { background: var(--accent-bg); filter: brightness(1.2); }
|
|
1622
|
+
.btn-confirm { flex: 1; background: var(--green); color: #0a0a0a; padding: 7px; font-size: 11px; }
|
|
1623
|
+
.btn-cancel { flex: 1; background: var(--bg-elevated); color: var(--text-muted); padding: 7px; font-size: 11px; }
|
|
1624
|
+
.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; }
|
|
1625
|
+
.save-input:focus { border-color: var(--accent); outline: none; }
|
|
696
1626
|
|
|
697
1627
|
/* Variant cards */
|
|
698
|
-
.variant-card { background:
|
|
699
|
-
.variant-card:hover { border-color:
|
|
700
|
-
.variant-card .vname { font-size: 12px; font-weight: 600; color:
|
|
701
|
-
.variant-card .vdesc { font-size: 10px; color:
|
|
702
|
-
.variant-card .vmeta { font-size: 10px; color:
|
|
703
|
-
.variant-card .vmeta .vresult { color:
|
|
704
|
-
.variant-card .vdelete { position: absolute; top: 6px; right: 8px; color:
|
|
1628
|
+
.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; }
|
|
1629
|
+
.variant-card:hover { border-color: var(--green); }
|
|
1630
|
+
.variant-card .vname { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
1631
|
+
.variant-card .vdesc { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
|
1632
|
+
.variant-card .vmeta { font-size: 10px; color: var(--text-faint); margin-top: 4px; }
|
|
1633
|
+
.variant-card .vmeta .vresult { color: var(--green); }
|
|
1634
|
+
.variant-card .vdelete { position: absolute; top: 6px; right: 8px; color: var(--red); font-size: 10px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
|
|
705
1635
|
.variant-card:hover .vdelete { opacity: 1; }
|
|
706
|
-
.variant-card .vbase { display: inline-block; font-size: 9px; padding: 1px 5px; background:
|
|
1636
|
+
.variant-card .vbase { display: inline-block; font-size: 9px; padding: 1px 5px; background: var(--accent-bg); color: var(--accent); border-radius: 3px; margin-top: 3px; }
|
|
707
1637
|
|
|
708
1638
|
/* RIGHT PANEL — Simulation viewer */
|
|
709
1639
|
.viewer { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
|
|
710
|
-
.viewer-top { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background:
|
|
711
|
-
.viewer-mid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background:
|
|
712
|
-
.viewer-bottom { background:
|
|
1640
|
+
.viewer-top { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); }
|
|
1641
|
+
.viewer-mid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); overflow: hidden; }
|
|
1642
|
+
.viewer-bottom { background: var(--bg-primary); border-top: 1px solid var(--border); padding: 12px 16px; max-height: 180px; overflow-y: auto; }
|
|
713
1643
|
|
|
714
|
-
.vpanel { background:
|
|
715
|
-
.vpanel h2 { font-size: 11px; color:
|
|
1644
|
+
.vpanel { background: var(--bg-primary); padding: 14px; overflow-y: auto; }
|
|
1645
|
+
.vpanel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
|
716
1646
|
|
|
717
1647
|
/* Metrics */
|
|
718
1648
|
.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
|
|
719
|
-
.metric-box { background:
|
|
720
|
-
.metric-box .value { font-size: 20px; font-weight: 700; color:
|
|
721
|
-
.metric-box .label { font-size: 10px; color:
|
|
722
|
-
.metric-box.good .value { color:
|
|
723
|
-
.metric-box.bad .value { color:
|
|
724
|
-
.metric-box.warn .value { color:
|
|
1649
|
+
.metric-box { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 10px; text-align: center; }
|
|
1650
|
+
.metric-box .value { font-size: 20px; font-weight: 700; color: var(--text-primary); }
|
|
1651
|
+
.metric-box .label { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
|
1652
|
+
.metric-box.good .value { color: var(--green); }
|
|
1653
|
+
.metric-box.bad .value { color: var(--red); }
|
|
1654
|
+
.metric-box.warn .value { color: var(--yellow); }
|
|
725
1655
|
|
|
726
1656
|
/* Agent bars */
|
|
727
1657
|
.agent-row { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; }
|
|
728
|
-
.agent-name { width: 130px; color:
|
|
729
|
-
.impact-bar-bg { flex: 1; height: 14px; background:
|
|
1658
|
+
.agent-name { width: 130px; color: var(--text-secondary); flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1659
|
+
.impact-bar-bg { flex: 1; height: 14px; background: var(--bg-surface); border-radius: 3px; position: relative; overflow: hidden; }
|
|
730
1660
|
.impact-bar { height: 100%; border-radius: 3px; transition: width 0.4s ease; position: absolute; top: 0; }
|
|
731
|
-
.impact-bar.positive { background:
|
|
732
|
-
.impact-bar.negative { background:
|
|
733
|
-
.impact-val { width: 44px; text-align: right; color:
|
|
734
|
-
.center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background:
|
|
1661
|
+
.impact-bar.positive { background: var(--green); right: 50%; }
|
|
1662
|
+
.impact-bar.negative { background: var(--red); left: 50%; }
|
|
1663
|
+
.impact-val { width: 44px; text-align: right; color: var(--text-secondary); font-size: 10px; }
|
|
1664
|
+
.center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
|
|
735
1665
|
.verdict { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-left: 4px; }
|
|
736
|
-
.verdict.ALLOW { background:
|
|
737
|
-
.verdict.BLOCK { background:
|
|
738
|
-
.verdict.PAUSE { background:
|
|
1666
|
+
.verdict.ALLOW { background: var(--green-bg); color: var(--green); }
|
|
1667
|
+
.verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1668
|
+
.verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
|
|
739
1669
|
|
|
740
1670
|
/* Chart */
|
|
741
1671
|
.chart-container { position: relative; height: 100%; min-height: 150px; }
|
|
742
1672
|
canvas { width: 100% !important; height: 100% !important; }
|
|
743
1673
|
|
|
744
1674
|
/* Simulation Trace */
|
|
745
|
-
.trace-round { margin-bottom: 10px; border: 1px solid
|
|
746
|
-
.trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background:
|
|
747
|
-
.trace-round-header:hover { background:
|
|
1675
|
+
.trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
1676
|
+
.trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
|
|
1677
|
+
.trace-round-header:hover { background: var(--bg-elevated); }
|
|
748
1678
|
.trace-phase { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
749
|
-
.trace-phase.baseline { background:
|
|
750
|
-
.trace-phase.governed { background:
|
|
751
|
-
.trace-round-label { font-size: 11px; color:
|
|
752
|
-
.trace-round-metrics { margin-left: auto; font-size: 10px; color:
|
|
1679
|
+
.trace-phase.baseline { background: var(--blue-bg); color: var(--blue); }
|
|
1680
|
+
.trace-phase.governed { background: var(--green-bg); color: var(--green); }
|
|
1681
|
+
.trace-round-label { font-size: 11px; color: var(--text-primary); font-weight: 600; }
|
|
1682
|
+
.trace-round-metrics { margin-left: auto; font-size: 10px; color: var(--text-muted); display: flex; gap: 10px; }
|
|
753
1683
|
.trace-body { padding: 0 10px 8px; }
|
|
754
1684
|
.trace-body[data-collapsed="true"] { display: none; }
|
|
755
1685
|
.trace-section { margin-top: 6px; }
|
|
756
|
-
.trace-section-label { font-size: 9px; color:
|
|
1686
|
+
.trace-section-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 3px; display: flex; align-items: center; gap: 5px; }
|
|
757
1687
|
.trace-section-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
|
|
758
1688
|
.trace-section-label.event::before { background: #f59e0b; }
|
|
759
1689
|
.trace-section-label.agents::before { background: #3b82f6; }
|
|
760
1690
|
.trace-section-label.governance::before { background: #10b981; }
|
|
761
|
-
.trace-event-item { font-size: 10px; color:
|
|
1691
|
+
.trace-event-item { font-size: 10px; color: var(--text-primary); padding: 3px 0 3px 11px; border-left: 2px solid #f59e0b; margin-left: 2px; }
|
|
762
1692
|
.trace-event-severity { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; margin-left: 4px; }
|
|
763
|
-
.trace-event-severity.major, .trace-event-severity.extreme { background:
|
|
764
|
-
.trace-event-severity.moderate { background:
|
|
765
|
-
.trace-event-severity.minor { background:
|
|
1693
|
+
.trace-event-severity.major, .trace-event-severity.extreme { background: var(--red-bg); color: var(--red); }
|
|
1694
|
+
.trace-event-severity.moderate { background: var(--yellow-bg); color: var(--yellow); }
|
|
1695
|
+
.trace-event-severity.minor { background: var(--green-bg); color: var(--green); }
|
|
766
1696
|
.trace-agent-item { font-size: 10px; padding: 2px 0 2px 11px; border-left: 2px solid #3b82f6; margin-left: 2px; display: flex; align-items: center; gap: 6px; }
|
|
767
|
-
.trace-agent-name { color:
|
|
768
|
-
.trace-agent-action { color:
|
|
1697
|
+
.trace-agent-name { color: var(--blue); font-weight: 500; min-width: 80px; }
|
|
1698
|
+
.trace-agent-action { color: var(--text-secondary); flex: 1; }
|
|
769
1699
|
.trace-agent-impact { font-size: 9px; font-weight: 600; min-width: 36px; text-align: right; }
|
|
770
|
-
.trace-agent-impact.positive { color:
|
|
771
|
-
.trace-agent-impact.negative { color:
|
|
1700
|
+
.trace-agent-impact.positive { color: var(--green); }
|
|
1701
|
+
.trace-agent-impact.negative { color: var(--red); }
|
|
772
1702
|
.trace-gov-item { font-size: 10px; padding: 3px 6px 3px 11px; border-left: 2px solid #10b981; margin-left: 2px; display: flex; align-items: center; gap: 6px; }
|
|
773
|
-
.trace-gov-rule { font-size: 9px; color:
|
|
774
|
-
.trace-gov-reason { color:
|
|
775
|
-
.trace-dynamics { font-size: 10px; color:
|
|
776
|
-
.trace-arrow { color:
|
|
777
|
-
.trace-empty { font-size: 10px; color:
|
|
1703
|
+
.trace-gov-rule { font-size: 9px; color: var(--text-muted); font-family: monospace; }
|
|
1704
|
+
.trace-gov-reason { color: var(--text-secondary); flex: 1; }
|
|
1705
|
+
.trace-dynamics { font-size: 10px; color: var(--purple); padding: 2px 0 2px 11px; border-left: 2px solid #7c3aed; margin-left: 2px; font-style: italic; }
|
|
1706
|
+
.trace-arrow { color: var(--text-faint); font-size: 10px; text-align: center; padding: 2px 0; }
|
|
1707
|
+
.trace-empty { font-size: 10px; color: var(--text-faint); font-style: italic; padding: 4px 0; }
|
|
778
1708
|
|
|
779
1709
|
/* World info */
|
|
780
|
-
.world-thesis { font-size: 11px; color:
|
|
781
|
-
.
|
|
1710
|
+
.world-thesis { font-size: 11px; color: var(--text-secondary); font-style: italic; margin-bottom: 8px; }
|
|
1711
|
+
.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; }
|
|
1712
|
+
.rule-card:hover { border-color: var(--text-muted); }
|
|
1713
|
+
.rule-card.type-invariant { border-left: 4px solid var(--green); }
|
|
1714
|
+
.rule-card.type-gate { border-left: 4px solid var(--red); }
|
|
1715
|
+
.rule-card.type-warning { border-left: 4px solid var(--yellow); }
|
|
1716
|
+
.rule-card.type-modify { border-left: 4px solid #60a5fa; }
|
|
1717
|
+
.rule-card .rule-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
1718
|
+
.rule-card .rule-icon { font-size: 12px; flex-shrink: 0; }
|
|
1719
|
+
.rule-card .rule-title { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
1720
|
+
.rule-card .rule-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
|
|
1721
|
+
.rule-card .rule-meta { font-size: 10px; color: var(--text-muted); margin-top: 6px; opacity: 0.8; }
|
|
1722
|
+
.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); }
|
|
1723
|
+
.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)); }
|
|
1724
|
+
.rule-card .rule-impact.visible { display: block; }
|
|
1725
|
+
.rule-card .rule-impact .impact-stat { color: var(--text-primary); font-weight: 600; }
|
|
1726
|
+
.rule-card .rule-impact .impact-label { color: var(--text-muted); }
|
|
1727
|
+
.rule-card.user-rule { border-left-color: #818cf8; background: rgba(129, 140, 248, 0.05); }
|
|
1728
|
+
.rule-source-tag { font-size: 9px; font-weight: 700; color: #818cf8; background: rgba(129, 140, 248, 0.15); padding: 1px 5px; border-radius: 3px; margin-left: auto; letter-spacing: 0.5px; }
|
|
782
1729
|
|
|
783
1730
|
/* Empty state */
|
|
784
|
-
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color:
|
|
1731
|
+
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-faint); }
|
|
785
1732
|
.empty-state .icon { font-size: 48px; margin-bottom: 12px; }
|
|
786
|
-
.empty-state .msg { font-size: 13px; }
|
|
787
|
-
.empty-state .hint { font-size: 11px; color:
|
|
1733
|
+
.empty-state .msg { font-size: 13px; color: var(--text-muted); }
|
|
1734
|
+
.empty-state .hint { font-size: 11px; color: var(--text-faint); margin-top: 6px; }
|
|
788
1735
|
|
|
789
1736
|
/* System Shift Card */
|
|
790
|
-
.system-shift { display: none; margin: 12px 16px; border: 1px solid
|
|
1737
|
+
.system-shift { display: none; margin: 12px 16px; border: 1px solid var(--green-bg); border-radius: 8px; background: var(--bg-secondary); overflow: hidden; animation: fadeInUp 0.4s ease; }
|
|
791
1738
|
.system-shift.visible { display: block; }
|
|
792
1739
|
@keyframes fadeInUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
793
|
-
.ss-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background:
|
|
794
|
-
.ss-icon { width: 8px; height: 8px; border-radius: 50%; background:
|
|
795
|
-
.ss-title { font-size: 11px; font-weight: 700; color:
|
|
796
|
-
.ss-rule { font-size: 13px; font-weight: 600; color:
|
|
1740
|
+
.ss-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background: var(--green-bg); border-bottom: 1px solid var(--green-bg); }
|
|
1741
|
+
.ss-icon { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgba(74,222,128,0.4); }
|
|
1742
|
+
.ss-title { font-size: 11px; font-weight: 700; color: var(--green); text-transform: uppercase; letter-spacing: 1.5px; }
|
|
1743
|
+
.ss-rule { font-size: 13px; font-weight: 600; color: var(--text-primary); padding: 10px 14px 0; }
|
|
797
1744
|
.ss-body { padding: 10px 14px 14px; display: grid; gap: 8px; }
|
|
798
|
-
.ss-section { background:
|
|
799
|
-
.ss-section-label { font-size: 9px; font-weight: 700; color:
|
|
800
|
-
.ss-adapt-rate { font-size: 20px; font-weight: 700; color:
|
|
801
|
-
.ss-adapt-desc { font-size: 11px; color:
|
|
802
|
-
.ss-shift-item { font-size: 11px; color:
|
|
803
|
-
.ss-shift-arrow { color:
|
|
804
|
-
.ss-pattern-tag { display: inline-block; font-size: 10px; padding: 2px 8px; background:
|
|
805
|
-
.ss-impact-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color:
|
|
806
|
-
.ss-impact-delta { color:
|
|
807
|
-
.ss-impact-delta.negative { color:
|
|
808
|
-
.ss-narrative { font-size: 12px; color:
|
|
809
|
-
.ss-scale { font-size: 10px; color:
|
|
810
|
-
.ss-scale strong { color:
|
|
811
|
-
.ss-flow { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 10px; color:
|
|
812
|
-
.ss-flow-arrow { color:
|
|
813
|
-
.ss-raw-toggle { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background:
|
|
814
|
-
.ss-raw-toggle:hover { color:
|
|
1745
|
+
.ss-section { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
|
|
1746
|
+
.ss-section-label { font-size: 9px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
|
|
1747
|
+
.ss-adapt-rate { font-size: 20px; font-weight: 700; color: var(--green); }
|
|
1748
|
+
.ss-adapt-desc { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
|
|
1749
|
+
.ss-shift-item { font-size: 11px; color: var(--text-secondary); padding: 2px 0; display: flex; align-items: center; gap: 6px; }
|
|
1750
|
+
.ss-shift-arrow { color: var(--green); font-weight: 600; }
|
|
1751
|
+
.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; }
|
|
1752
|
+
.ss-impact-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--text-secondary); padding: 2px 0; }
|
|
1753
|
+
.ss-impact-delta { color: var(--green); font-weight: 600; }
|
|
1754
|
+
.ss-impact-delta.negative { color: var(--red); }
|
|
1755
|
+
.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; }
|
|
1756
|
+
.ss-scale { font-size: 10px; color: var(--text-muted); padding: 0 14px 6px; }
|
|
1757
|
+
.ss-scale strong { color: var(--text-secondary); }
|
|
1758
|
+
.ss-flow { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 10px; color: var(--text-faint); }
|
|
1759
|
+
.ss-flow-arrow { color: var(--green); }
|
|
1760
|
+
.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; }
|
|
1761
|
+
.ss-raw-toggle:hover { color: var(--text-secondary); }
|
|
815
1762
|
.ss-raw-toggle .arrow { transition: transform 0.2s; }
|
|
816
1763
|
.ss-raw-toggle.open .arrow { transform: rotate(90deg); }
|
|
817
1764
|
.ss-raw-detail { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
|
|
818
1765
|
.ss-raw-detail.open { max-height: 200px; overflow-y: auto; }
|
|
819
1766
|
.ss-raw-list { padding: 6px 12px; }
|
|
820
|
-
.ss-raw-item { font-size: 10px; color:
|
|
821
|
-
.ss-raw-item .raw-agent { color:
|
|
822
|
-
.ss-raw-item .raw-action { color:
|
|
1767
|
+
.ss-raw-item { font-size: 10px; color: var(--text-muted); padding: 2px 0; display: flex; gap: 6px; }
|
|
1768
|
+
.ss-raw-item .raw-agent { color: var(--text-secondary); min-width: 100px; }
|
|
1769
|
+
.ss-raw-item .raw-action { color: var(--text-secondary); flex: 1; }
|
|
823
1770
|
.ss-raw-item .raw-verdict { font-size: 9px; font-weight: 600; padding: 0 4px; border-radius: 2px; }
|
|
824
|
-
.ss-raw-item .raw-verdict.BLOCK { background:
|
|
825
|
-
.ss-raw-item .raw-verdict.MODIFY { background:
|
|
1771
|
+
.ss-raw-item .raw-verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1772
|
+
.ss-raw-item .raw-verdict.MODIFY { background: var(--yellow-bg); color: var(--yellow); }
|
|
826
1773
|
|
|
827
1774
|
/* Integration Quick-Start (in controls panel) */
|
|
828
|
-
.integrate-section { background:
|
|
829
|
-
.integrate-section h4 { font-size: 10px; color:
|
|
830
|
-
.integrate-code { font-size: 10px; color:
|
|
831
|
-
.integrate-code .kw { color:
|
|
832
|
-
.integrate-code .str { color:
|
|
833
|
-
.integrate-code .comment { color:
|
|
834
|
-
.integrate-endpoint { font-size: 11px; color:
|
|
835
|
-
.integrate-endpoint code { color:
|
|
1775
|
+
.integrate-section { background: var(--accent-bg); border: 1px solid var(--accent-bg); border-radius: 6px; padding: 10px 12px; margin-top: 8px; }
|
|
1776
|
+
.integrate-section h4 { font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
|
1777
|
+
.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; }
|
|
1778
|
+
.integrate-code .kw { color: var(--accent); }
|
|
1779
|
+
.integrate-code .str { color: var(--green); }
|
|
1780
|
+
.integrate-code .comment { color: var(--text-muted); }
|
|
1781
|
+
.integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
|
|
1782
|
+
.integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
|
|
1783
|
+
|
|
1784
|
+
/* Rule editor */
|
|
1785
|
+
.rule-editor { margin-top: 8px; }
|
|
1786
|
+
.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; }
|
|
1787
|
+
.rule-input:focus { border-color: var(--accent); outline: none; }
|
|
1788
|
+
.rule-input::placeholder { color: var(--text-faint); }
|
|
1789
|
+
.btn-parse { background: var(--accent-bg); color: var(--accent); margin-top: 6px; padding: 8px; font-size: 11px; }
|
|
1790
|
+
.btn-parse:hover { filter: brightness(1.2); }
|
|
1791
|
+
.btn-parse:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1792
|
+
.parsed-rules { margin-top: 8px; }
|
|
1793
|
+
.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; }
|
|
1794
|
+
.parsed-rule:hover { border-color: var(--text-muted); }
|
|
1795
|
+
.parsed-rule.enforcement-block { border-left: 4px solid var(--red); }
|
|
1796
|
+
.parsed-rule.enforcement-allow { border-left: 4px solid var(--green); }
|
|
1797
|
+
.parsed-rule.enforcement-modify { border-left: 4px solid #60a5fa; }
|
|
1798
|
+
.parsed-rule.enforcement-warn { border-left: 4px solid var(--yellow); }
|
|
1799
|
+
.parsed-rule .pr-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
1800
|
+
.parsed-rule .pr-icon { font-size: 12px; }
|
|
1801
|
+
.parsed-rule .pr-action { font-weight: 700; font-size: 12px; color: var(--text-primary); text-transform: capitalize; }
|
|
1802
|
+
.parsed-rule .pr-desc { color: var(--text-secondary); font-size: 11px; line-height: 1.4; }
|
|
1803
|
+
.parsed-rule .pr-patterns { color: var(--text-muted); font-size: 10px; margin-top: 6px; opacity: 0.8; }
|
|
1804
|
+
.btn-apply-rules { background: var(--green); color: #0a0a0a; margin-top: 6px; padding: 8px; font-size: 11px; }
|
|
1805
|
+
.btn-apply-rules:hover { filter: brightness(0.9); }
|
|
1806
|
+
.rule-status { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
|
|
1807
|
+
.rule-status.success { color: var(--green); }
|
|
1808
|
+
.rule-status.error { color: var(--red); }
|
|
1809
|
+
.rule-examples { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.6; }
|
|
1810
|
+
.rule-examples code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
|
|
1811
|
+
|
|
1812
|
+
/* World Action Bar */
|
|
1813
|
+
.world-action-bar { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
1814
|
+
.btn-world-action { flex: 1; min-width: 0; padding: 6px 4px; font-size: 10px; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font-family: inherit; text-align: center; transition: all 0.2s; white-space: nowrap; }
|
|
1815
|
+
.btn-world-action:hover { border-color: var(--accent); color: var(--accent); }
|
|
1816
|
+
.btn-world-action.btn-export { color: var(--green); border-color: var(--green); opacity: 0.7; }
|
|
1817
|
+
.btn-world-action.btn-export:hover { opacity: 1; }
|
|
1818
|
+
|
|
1819
|
+
/* World Source Tabs */
|
|
1820
|
+
.world-source-tabs { display: flex; gap: 4px; }
|
|
1821
|
+
.ws-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 10px 6px; background: var(--bg-surface); border: 2px solid var(--border); border-radius: 6px; cursor: pointer; transition: all 0.2s; text-align: center; }
|
|
1822
|
+
.ws-tab:hover { border-color: var(--text-muted); }
|
|
1823
|
+
.ws-tab.active { border-color: var(--accent); background: var(--accent-bg); }
|
|
1824
|
+
.ws-tab input[type="radio"] { display: none; }
|
|
1825
|
+
.ws-label { font-size: 11px; font-weight: 700; color: var(--text-primary); }
|
|
1826
|
+
.ws-hint { font-size: 9px; color: var(--text-muted); margin-top: 2px; }
|
|
1827
|
+
.ws-tab.active .ws-label { color: var(--accent); }
|
|
1828
|
+
|
|
1829
|
+
/* World Source Panels */
|
|
1830
|
+
.world-source-panel { animation: fadeIn 0.2s ease; }
|
|
1831
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
1832
|
+
|
|
1833
|
+
/* Custom World Header */
|
|
1834
|
+
.custom-world-header { margin-bottom: 12px; }
|
|
1835
|
+
.world-name-input { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
|
|
1836
|
+
.world-name-input:focus { border-color: var(--accent); outline: none; }
|
|
1837
|
+
.world-thesis-input { width: 100%; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 11px; resize: none; height: 36px; }
|
|
1838
|
+
.world-thesis-input:focus { border-color: var(--accent); outline: none; }
|
|
1839
|
+
|
|
1840
|
+
/* Rule editor enhancements */
|
|
1841
|
+
.rule-editor-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600; }
|
|
1842
|
+
.rule-input-large { min-height: 120px; }
|
|
1843
|
+
.btn-generate-world { background: var(--accent); color: #fff; margin-top: 8px; padding: 10px; font-size: 12px; font-weight: 700; width: 100%; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
|
|
1844
|
+
.btn-generate-world:hover { filter: brightness(1.1); }
|
|
1845
|
+
|
|
1846
|
+
/* Upload Zone */
|
|
1847
|
+
.upload-zone { border: 2px dashed var(--border-subtle); border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.2s; margin-bottom: 12px; }
|
|
1848
|
+
.upload-zone:hover, .upload-zone.dragover { border-color: var(--accent); background: var(--accent-bg); }
|
|
1849
|
+
.upload-icon { font-size: 28px; margin-bottom: 8px; }
|
|
1850
|
+
.upload-label { font-size: 12px; color: var(--text-secondary); }
|
|
1851
|
+
.upload-or { font-size: 10px; color: var(--text-faint); margin: 8px 0; }
|
|
1852
|
+
.btn-upload-browse { background: var(--bg-elevated); color: var(--text-primary); border: 1px solid var(--border); padding: 6px 16px; border-radius: 4px; cursor: pointer; font-family: inherit; font-size: 11px; }
|
|
1853
|
+
.upload-paste-section { margin-bottom: 12px; }
|
|
1854
|
+
.btn-load-world { background: var(--green); color: #0a0a0a; width: 100%; padding: 10px; font-size: 12px; font-weight: 700; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
|
|
1855
|
+
.btn-load-world:hover { filter: brightness(0.9); }
|
|
1856
|
+
|
|
1857
|
+
/* Loaded World Card */
|
|
1858
|
+
.loaded-world-card { background: var(--green-bg); border: 1px solid var(--green); border-radius: 6px; padding: 12px; margin-top: 10px; }
|
|
1859
|
+
.lw-name { font-size: 13px; font-weight: 700; color: var(--green); }
|
|
1860
|
+
.lw-thesis { font-size: 11px; color: var(--text-secondary); margin-top: 4px; font-style: italic; }
|
|
1861
|
+
.lw-stats { font-size: 10px; color: var(--text-muted); margin-top: 6px; }
|
|
1862
|
+
|
|
1863
|
+
/* Schema Reference */
|
|
1864
|
+
.schema-ref { font-size: 10px; color: var(--text-muted); }
|
|
1865
|
+
.schema-item { padding: 3px 0; border-bottom: 1px solid var(--border); }
|
|
1866
|
+
.schema-item:last-child { border-bottom: none; }
|
|
1867
|
+
.schema-item code { color: var(--accent); background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; }
|
|
836
1868
|
</style>
|
|
837
1869
|
</head>
|
|
838
1870
|
<body>
|
|
839
1871
|
<div class="header">
|
|
840
1872
|
<div style="display:flex;align-items:center">
|
|
841
1873
|
<h1>NV-SIM</h1>
|
|
842
|
-
<span class="sub">
|
|
1874
|
+
<span class="sub">Governance Runtime</span>
|
|
1875
|
+
</div>
|
|
1876
|
+
<div class="header-right">
|
|
1877
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">Light Mode</button>
|
|
1878
|
+
<span id="status" class="status idle">Ready</span>
|
|
843
1879
|
</div>
|
|
844
|
-
<span id="status" class="status idle">Ready</span>
|
|
845
1880
|
</div>
|
|
846
1881
|
|
|
847
1882
|
<div class="layout">
|
|
848
1883
|
<!-- LEFT: CONTROLS -->
|
|
849
1884
|
<div class="controls" id="controls-panel">
|
|
850
|
-
<!--
|
|
1885
|
+
<!-- World Action Bar -->
|
|
1886
|
+
<div class="world-action-bar">
|
|
1887
|
+
<button class="btn btn-world-action" id="new-world-btn" title="Clear everything and start fresh">+ New World</button>
|
|
1888
|
+
<button class="btn btn-world-action" id="load-file-btn" title="Load a .json world file">Load World File</button>
|
|
1889
|
+
<button class="btn btn-world-action" id="clear-rules-btn" title="Clear custom rules only">Clear Rules</button>
|
|
1890
|
+
<button class="btn btn-world-action btn-export" id="export-world-btn" title="Export current world as JSON">Save as World File</button>
|
|
1891
|
+
</div>
|
|
1892
|
+
|
|
1893
|
+
<!-- World Source selector -->
|
|
851
1894
|
<div class="ctrl-section">
|
|
852
|
-
<h3>
|
|
853
|
-
<div class="
|
|
854
|
-
<
|
|
855
|
-
<
|
|
856
|
-
|
|
1895
|
+
<h3>World Source</h3>
|
|
1896
|
+
<div class="world-source-tabs">
|
|
1897
|
+
<label class="ws-tab active" data-source="preset">
|
|
1898
|
+
<input type="radio" name="world-source" value="preset" checked>
|
|
1899
|
+
<span class="ws-label">Preset</span>
|
|
1900
|
+
<span class="ws-hint">Demo scenarios</span>
|
|
1901
|
+
</label>
|
|
1902
|
+
<label class="ws-tab" data-source="custom">
|
|
1903
|
+
<input type="radio" name="world-source" value="custom">
|
|
1904
|
+
<span class="ws-label">Custom Rules</span>
|
|
1905
|
+
<span class="ws-hint">Define your world</span>
|
|
1906
|
+
</label>
|
|
1907
|
+
<label class="ws-tab" data-source="upload">
|
|
1908
|
+
<input type="radio" name="world-source" value="upload">
|
|
1909
|
+
<span class="ws-label">World File</span>
|
|
1910
|
+
<span class="ws-hint">JSON / .nv-world</span>
|
|
1911
|
+
</label>
|
|
857
1912
|
</div>
|
|
858
|
-
<div id="engine-status" style="font-size:10px;color:#555;margin-top:4px"></div>
|
|
859
1913
|
</div>
|
|
860
1914
|
|
|
861
|
-
<!--
|
|
862
|
-
<div class="
|
|
863
|
-
<
|
|
864
|
-
|
|
865
|
-
<
|
|
1915
|
+
<!-- SOURCE: Preset -->
|
|
1916
|
+
<div class="world-source-panel" id="source-preset">
|
|
1917
|
+
<div class="ctrl-section">
|
|
1918
|
+
<h3>World</h3>
|
|
1919
|
+
<div class="ctrl-row">
|
|
1920
|
+
<select id="world-select"></select>
|
|
1921
|
+
</div>
|
|
1922
|
+
<div id="world-thesis" class="world-thesis"></div>
|
|
1923
|
+
</div>
|
|
1924
|
+
|
|
1925
|
+
<!-- State variables (dynamic sliders) -->
|
|
1926
|
+
<div class="ctrl-section" id="state-vars-section" style="display:none">
|
|
1927
|
+
<h3>World Rules</h3>
|
|
1928
|
+
<div id="state-vars"></div>
|
|
1929
|
+
</div>
|
|
1930
|
+
|
|
1931
|
+
<!-- Scenario presets -->
|
|
1932
|
+
<div class="ctrl-section">
|
|
1933
|
+
<h3>Scenarios</h3>
|
|
1934
|
+
<div id="scenario-list"></div>
|
|
866
1935
|
</div>
|
|
867
|
-
<div id="world-thesis" class="world-thesis"></div>
|
|
868
1936
|
</div>
|
|
869
1937
|
|
|
870
|
-
<!--
|
|
871
|
-
<div class="
|
|
872
|
-
<
|
|
873
|
-
|
|
1938
|
+
<!-- SOURCE: Custom Rules (Define Your World) -->
|
|
1939
|
+
<div class="world-source-panel" id="source-custom" style="display:none">
|
|
1940
|
+
<div class="ctrl-section">
|
|
1941
|
+
<h3>Define Your World</h3>
|
|
1942
|
+
<div class="custom-world-header">
|
|
1943
|
+
<input type="text" class="world-name-input" id="custom-world-name" placeholder="World name (e.g. Marketing Governance)">
|
|
1944
|
+
<textarea class="world-thesis-input" id="custom-world-thesis" placeholder="What is this world about? (thesis)"></textarea>
|
|
1945
|
+
</div>
|
|
1946
|
+
<div class="rule-editor">
|
|
1947
|
+
<div class="rule-editor-label">Type your governance rules:</div>
|
|
1948
|
+
<textarea class="rule-input rule-input-large" id="rule-input" placeholder="No agent may spend more than $10k without approval All outbound emails must be reviewed Block deletion of production data Limit API calls to 100 per minute Require manager approval for refunds over $500"></textarea>
|
|
1949
|
+
<button class="btn btn-generate-world" id="parse-rules-btn">Generate World</button>
|
|
1950
|
+
<div id="parsed-rules" class="parsed-rules"></div>
|
|
1951
|
+
<div id="rule-status" class="rule-status"></div>
|
|
1952
|
+
<div class="rule-examples">
|
|
1953
|
+
Rule patterns:<br>
|
|
1954
|
+
<code>Block [action]</code> — hard suppression<br>
|
|
1955
|
+
<code>Limit [X] to [N]</code> — cap extremes<br>
|
|
1956
|
+
<code>Require [X] for [Y]</code> — structural constraint<br>
|
|
1957
|
+
<code>Pause [X] for review</code> — human-in-the-loop<br>
|
|
1958
|
+
<code>Allow [X]</code> — explicit permission<br>
|
|
1959
|
+
<code>Monitor [X]</code> — circuit breaker gate
|
|
1960
|
+
</div>
|
|
1961
|
+
</div>
|
|
1962
|
+
</div>
|
|
1963
|
+
|
|
1964
|
+
<!-- Base world (optional) -->
|
|
1965
|
+
<div class="ctrl-section">
|
|
1966
|
+
<h3>Base World (Optional)</h3>
|
|
1967
|
+
<div class="ctrl-row">
|
|
1968
|
+
<select id="custom-base-world">
|
|
1969
|
+
<option value="">None — start from scratch</option>
|
|
1970
|
+
</select>
|
|
1971
|
+
<div style="font-size:10px;color:var(--text-faint);margin-top:4px">Layer your rules on top of a preset world</div>
|
|
1972
|
+
</div>
|
|
1973
|
+
</div>
|
|
874
1974
|
</div>
|
|
875
1975
|
|
|
876
|
-
<!--
|
|
877
|
-
<div class="
|
|
878
|
-
<
|
|
879
|
-
|
|
1976
|
+
<!-- SOURCE: Upload World File -->
|
|
1977
|
+
<div class="world-source-panel" id="source-upload" style="display:none">
|
|
1978
|
+
<div class="ctrl-section">
|
|
1979
|
+
<h3>Load World File</h3>
|
|
1980
|
+
<div class="upload-zone" id="upload-zone">
|
|
1981
|
+
<div class="upload-icon">📄</div>
|
|
1982
|
+
<div class="upload-label">Drop a .json or .nv-world file here</div>
|
|
1983
|
+
<div class="upload-or">or</div>
|
|
1984
|
+
<button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
|
|
1985
|
+
<input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
|
|
1986
|
+
</div>
|
|
1987
|
+
<div class="upload-paste-section">
|
|
1988
|
+
<div class="rule-editor-label">Or paste world JSON:</div>
|
|
1989
|
+
<textarea class="rule-input rule-input-large" id="world-json-input" placeholder='{ "name": "Marketing Governance", "thesis": "All marketing actions are governed", "invariants": [...], "rules": [...], "gates": [...] }'></textarea>
|
|
1990
|
+
</div>
|
|
1991
|
+
<button class="btn btn-load-world" id="load-world-btn">Load into Runtime</button>
|
|
1992
|
+
<div id="upload-status" class="rule-status"></div>
|
|
1993
|
+
|
|
1994
|
+
<!-- Loaded world info -->
|
|
1995
|
+
<div id="loaded-world-info" style="display:none">
|
|
1996
|
+
<div class="loaded-world-card">
|
|
1997
|
+
<div class="lw-name" id="lw-name"></div>
|
|
1998
|
+
<div class="lw-thesis" id="lw-thesis"></div>
|
|
1999
|
+
<div class="lw-stats" id="lw-stats"></div>
|
|
2000
|
+
</div>
|
|
2001
|
+
</div>
|
|
2002
|
+
</div>
|
|
2003
|
+
|
|
2004
|
+
<!-- World file schema reference -->
|
|
2005
|
+
<div class="ctrl-section">
|
|
2006
|
+
<h3>World File Schema</h3>
|
|
2007
|
+
<div class="schema-ref">
|
|
2008
|
+
<div class="schema-item"><code>name</code> — world name</div>
|
|
2009
|
+
<div class="schema-item"><code>thesis</code> — what this world is about</div>
|
|
2010
|
+
<div class="schema-item"><code>rules[]</code> — governance rules (plain English or structured)</div>
|
|
2011
|
+
<div class="schema-item"><code>invariants[]</code> — rules that always hold <code>{id, description}</code></div>
|
|
2012
|
+
<div class="schema-item"><code>gates[]</code> — viability thresholds <code>{id, label, condition, severity}</code></div>
|
|
2013
|
+
<div class="schema-item"><code>state_variables[]</code> — sliders <code>{id, label, type, range, default_value}</code></div>
|
|
2014
|
+
</div>
|
|
2015
|
+
</div>
|
|
2016
|
+
</div>
|
|
2017
|
+
|
|
2018
|
+
<!-- Simulation Engine (demoted, below world source) -->
|
|
2019
|
+
<div class="ctrl-section" style="margin-top:8px">
|
|
2020
|
+
<h3>Engine</h3>
|
|
2021
|
+
<div class="ctrl-row">
|
|
2022
|
+
<select id="engine-select">
|
|
2023
|
+
<option value="nv-sim" selected>NV-SIM (Built-in)</option>
|
|
2024
|
+
</select>
|
|
2025
|
+
</div>
|
|
2026
|
+
<div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
|
|
880
2027
|
</div>
|
|
881
2028
|
|
|
882
2029
|
<!-- Narrative injection -->
|
|
@@ -951,6 +2098,24 @@ verdict = evaluate(
|
|
|
951
2098
|
<span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
|
|
952
2099
|
</div>
|
|
953
2100
|
</div>
|
|
2101
|
+
|
|
2102
|
+
<!-- Session Report Panel -->
|
|
2103
|
+
<div class="ctrl-section" id="session-panel">
|
|
2104
|
+
<h3 class="ctrl-label">SESSION</h3>
|
|
2105
|
+
<div class="metric-grid" style="grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
|
|
2106
|
+
<div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
|
|
2107
|
+
<div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
|
|
2108
|
+
<div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
|
|
2109
|
+
<div class="metric-box"><div class="value" id="s-allowed" style="color:#4ade80">0</div><div class="label">Allowed</div></div>
|
|
2110
|
+
</div>
|
|
2111
|
+
<div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
|
|
2112
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
2113
|
+
<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>
|
|
2114
|
+
<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>
|
|
2115
|
+
<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>
|
|
2116
|
+
</div>
|
|
2117
|
+
<div id="session-history" style="margin-top:6px;font-size:10px;color:#666"></div>
|
|
2118
|
+
</div>
|
|
954
2119
|
</div>
|
|
955
2120
|
</div>
|
|
956
2121
|
|
|
@@ -1050,6 +2215,7 @@ let governedImpacts = [];
|
|
|
1050
2215
|
let chartLabels = [];
|
|
1051
2216
|
let chart = null;
|
|
1052
2217
|
let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
|
|
2218
|
+
let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
|
|
1053
2219
|
|
|
1054
2220
|
const statusEl = document.getElementById('status');
|
|
1055
2221
|
const worldSelect = document.getElementById('world-select');
|
|
@@ -1123,6 +2289,9 @@ async function init() {
|
|
|
1123
2289
|
// Load saved variants
|
|
1124
2290
|
await loadVariants();
|
|
1125
2291
|
|
|
2292
|
+
// Populate base world selector for custom rules mode
|
|
2293
|
+
populateBaseWorldSelect();
|
|
2294
|
+
|
|
1126
2295
|
// Connect SSE
|
|
1127
2296
|
connectSSE();
|
|
1128
2297
|
}
|
|
@@ -1321,10 +2490,10 @@ function initChart() {
|
|
|
1321
2490
|
animation: { duration: 400 },
|
|
1322
2491
|
responsive: true,
|
|
1323
2492
|
maintainAspectRatio: false,
|
|
1324
|
-
plugins: { legend: { labels: { color: '#888', font: { family: 'monospace', size: 10 } } } },
|
|
2493
|
+
plugins: { legend: { labels: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888', font: { family: 'monospace', size: 10 } } } },
|
|
1325
2494
|
scales: {
|
|
1326
|
-
x: { ticks: { color: '#
|
|
1327
|
-
y: { ticks: { color: '#
|
|
2495
|
+
x: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' } },
|
|
2496
|
+
y: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' }, min: -1, max: 1 }
|
|
1328
2497
|
}
|
|
1329
2498
|
}
|
|
1330
2499
|
});
|
|
@@ -1461,11 +2630,65 @@ function handleEvent(event) {
|
|
|
1461
2630
|
if (!narrativeEventsByRound[ev.round]) narrativeEventsByRound[ev.round] = [];
|
|
1462
2631
|
narrativeEventsByRound[ev.round].push(ev);
|
|
1463
2632
|
});
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
2633
|
+
function parseRuleTitle(id, text) {
|
|
2634
|
+
var parts = text.split(/\s*[—–-]{1,}\s*/);
|
|
2635
|
+
var title = parts.length > 1 ? parts[0].trim() : id.replace(/[-_]/g, ' ');
|
|
2636
|
+
var desc = parts.length > 1 ? parts.slice(1).join(' — ').trim() : text;
|
|
2637
|
+
title = title.replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
|
2638
|
+
return { title: title, desc: desc };
|
|
2639
|
+
}
|
|
2640
|
+
function generateWhy(text, type) {
|
|
2641
|
+
var t = text.toLowerCase();
|
|
2642
|
+
if (t.includes('liquidity') || t.includes('drain')) return 'Prevents system collapse from liquidity crises';
|
|
2643
|
+
if (t.includes('panic') || t.includes('cascade')) return 'Stops runaway feedback loops during market shocks';
|
|
2644
|
+
if (t.includes('leverage')) return 'Limits systemic risk from over-leveraged positions';
|
|
2645
|
+
if (t.includes('price') || t.includes('pricing')) return 'Stabilizes pricing mechanisms during volatility';
|
|
2646
|
+
if (t.includes('sentiment') || t.includes('consumer')) return 'Tracks behavioral feedback between price and confidence';
|
|
2647
|
+
if (t.includes('military') || t.includes('escalat')) return 'Models how escalation constrains available options';
|
|
2648
|
+
if (t.includes('diplomatic') || t.includes('window')) return 'Preserves negotiation pathways before they close';
|
|
2649
|
+
if (t.includes('grid') || t.includes('capacity')) return 'Prevents infrastructure overload from demand surges';
|
|
2650
|
+
if (t.includes('election') || t.includes('political')) return 'Captures how political pressure amplifies responses';
|
|
2651
|
+
if (t.includes('supply') || t.includes('energy') || t.includes('oil')) return 'Guards against cascading supply chain disruptions';
|
|
2652
|
+
if (t.includes('fraud') || t.includes('suspicious')) return 'Detects and contains anomalous behavior patterns';
|
|
2653
|
+
if (t.includes('withdraw') || t.includes('limit')) return 'Constrains individual actions to protect system stability';
|
|
2654
|
+
if (type === 'gate') return 'Blocks dangerous actions before they propagate';
|
|
2655
|
+
if (type === 'warning') return 'Provides early warning before thresholds are breached';
|
|
2656
|
+
return 'Maintains system integrity under stress conditions';
|
|
2657
|
+
}
|
|
2658
|
+
// Reset per-rule impact counters
|
|
2659
|
+
ruleImpactTracker = {};
|
|
2660
|
+
activeInvEl.innerHTML = event.invariants.map(function(inv) {
|
|
2661
|
+
var parsed = parseRuleTitle(inv.id, inv.description);
|
|
2662
|
+
var why = generateWhy(inv.description, 'invariant');
|
|
2663
|
+
var isUser = inv.source === 'user';
|
|
2664
|
+
var isFull = inv.enforcement === 'full';
|
|
2665
|
+
var enfLabel = isFull ? 'Fully enforced across system' : 'Advisory only';
|
|
2666
|
+
var enfIcon = isFull ? '✓' : '⚠';
|
|
2667
|
+
var sourceTag = isUser ? '<span class="rule-source-tag">USER RULE</span>' : '';
|
|
2668
|
+
ruleImpactTracker[inv.id] = { blocks: 0, label: parsed.title };
|
|
2669
|
+
return '<div class="rule-card type-invariant' + (isUser ? ' user-rule' : '') + '" data-rule-id="' + inv.id + '">' +
|
|
2670
|
+
'<div class="rule-header"><span class="rule-icon">🟢</span><span class="rule-title">' + parsed.title + '</span>' + sourceTag + '</div>' +
|
|
2671
|
+
'<div class="rule-desc">' + parsed.desc + '</div>' +
|
|
2672
|
+
'<div class="rule-meta">Invariant • ' + enfIcon + ' ' + enfLabel + '</div>' +
|
|
2673
|
+
'<div class="rule-why">' + why + '</div>' +
|
|
2674
|
+
'<div class="rule-impact" data-impact-id="' + inv.id + '"></div>' +
|
|
2675
|
+
'</div>';
|
|
2676
|
+
}).join('') + event.gates.map(function(g) {
|
|
2677
|
+
var isCritical = g.severity === 'critical';
|
|
2678
|
+
var typeClass = isCritical ? 'type-gate' : 'type-warning';
|
|
2679
|
+
var icon = isCritical ? '🔴' : '🟡';
|
|
2680
|
+
var typeLabel = isCritical ? 'Gate' : 'Warning';
|
|
2681
|
+
var effect = isCritical ? 'Blocks actions' : 'Signals risk';
|
|
2682
|
+
var why = generateWhy(g.label + ' ' + (g.condition || ''), isCritical ? 'gate' : 'warning');
|
|
2683
|
+
ruleImpactTracker[g.id] = { blocks: 0, label: g.label };
|
|
2684
|
+
return '<div class="rule-card ' + typeClass + '" data-rule-id="' + g.id + '">' +
|
|
2685
|
+
'<div class="rule-header"><span class="rule-icon">' + icon + '</span><span class="rule-title">' + g.label + '</span></div>' +
|
|
2686
|
+
'<div class="rule-desc">' + (g.condition || g.label) + '</div>' +
|
|
2687
|
+
'<div class="rule-meta">' + typeLabel + ' • ' + effect + '</div>' +
|
|
2688
|
+
'<div class="rule-why">' + why + '</div>' +
|
|
2689
|
+
'<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
|
|
2690
|
+
'</div>';
|
|
2691
|
+
}).join('');
|
|
1469
2692
|
initChart();
|
|
1470
2693
|
}
|
|
1471
2694
|
|
|
@@ -1502,6 +2725,8 @@ function handleEvent(event) {
|
|
|
1502
2725
|
document.getElementById('m-stability').parentElement.className = 'metric-box ' + (r.governed.metrics.stabilityScore > 0.7 ? 'good' : 'warn');
|
|
1503
2726
|
addLog('Complete. Governance effectiveness: ' + (r.comparison.governanceEffectiveness * 100).toFixed(0) + '%');
|
|
1504
2727
|
renderSystemShift(r);
|
|
2728
|
+
renderRuleImpacts(r);
|
|
2729
|
+
renderEnforcementClassification(r.enforcementClassification || []);
|
|
1505
2730
|
lastSimResult = {
|
|
1506
2731
|
stability: r.governed.metrics.stabilityScore,
|
|
1507
2732
|
volatility: r.governed.metrics.maxVolatility,
|
|
@@ -1514,6 +2739,59 @@ function handleEvent(event) {
|
|
|
1514
2739
|
}
|
|
1515
2740
|
}
|
|
1516
2741
|
|
|
2742
|
+
// ============================================
|
|
2743
|
+
// PER-RULE IMPACT RENDERING
|
|
2744
|
+
// ============================================
|
|
2745
|
+
function renderRuleImpacts(result) {
|
|
2746
|
+
var totalBlocks = shiftTracker.blocks;
|
|
2747
|
+
var cascadeAvoided = result && result.governed && result.baseline &&
|
|
2748
|
+
result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
|
|
2749
|
+
|
|
2750
|
+
Object.keys(ruleImpactTracker).forEach(function(ruleId) {
|
|
2751
|
+
var tracker = ruleImpactTracker[ruleId];
|
|
2752
|
+
var el = document.querySelector('[data-impact-id="' + ruleId + '"]');
|
|
2753
|
+
if (!el) return;
|
|
2754
|
+
|
|
2755
|
+
var html = '';
|
|
2756
|
+
if (tracker.blocks > 0) {
|
|
2757
|
+
html += '<span class="impact-stat">' + tracker.blocks + ' action' + (tracker.blocks > 1 ? 's' : '') + ' blocked</span>';
|
|
2758
|
+
if (cascadeAvoided) {
|
|
2759
|
+
html += ' <span class="impact-label">→ cascade avoided</span>';
|
|
2760
|
+
} else {
|
|
2761
|
+
html += ' <span class="impact-label">→ behavior modified</span>';
|
|
2762
|
+
}
|
|
2763
|
+
} else {
|
|
2764
|
+
// Rule was present but didn't fire — still useful info
|
|
2765
|
+
html += '<span class="impact-label">No violations detected — agents complied</span>';
|
|
2766
|
+
}
|
|
2767
|
+
el.innerHTML = html;
|
|
2768
|
+
el.classList.add('visible');
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// ============================================
|
|
2773
|
+
// ENFORCEMENT CLASSIFICATION — Full vs Advisory
|
|
2774
|
+
// ============================================
|
|
2775
|
+
function renderEnforcementClassification(entries) {
|
|
2776
|
+
if (!entries || entries.length === 0) return;
|
|
2777
|
+
entries.forEach(function(entry) {
|
|
2778
|
+
var card = document.querySelector('[data-rule-id="' + entry.id + '"]');
|
|
2779
|
+
if (!card) return;
|
|
2780
|
+
var metaEl = card.querySelector('.rule-meta');
|
|
2781
|
+
if (!metaEl) return;
|
|
2782
|
+
if (entry.level === 'full' && entry.fired) {
|
|
2783
|
+
metaEl.innerHTML = 'Invariant • ✓ Fully enforced • <span style="color:#22c55e;font-weight:600">FIRED</span>';
|
|
2784
|
+
card.style.borderLeftColor = '#22c55e';
|
|
2785
|
+
} else if (entry.level === 'full' && !entry.fired) {
|
|
2786
|
+
metaEl.innerHTML = 'Invariant • ✓ Fully enforced • <span style="color:var(--text-muted)">standby</span>';
|
|
2787
|
+
} else if (entry.level === 'advisory') {
|
|
2788
|
+
metaEl.innerHTML = 'Invariant • ⚠ Advisory only • <span style="color:var(--text-muted)">monitored</span>';
|
|
2789
|
+
card.style.borderLeftColor = '#6b7280';
|
|
2790
|
+
card.style.opacity = '0.75';
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
|
|
1517
2795
|
// ============================================
|
|
1518
2796
|
// SYSTEM SHIFT CARD — The Demo Moment
|
|
1519
2797
|
// ============================================
|
|
@@ -1551,6 +2829,18 @@ function trackShift(event) {
|
|
|
1551
2829
|
governed.forEach(function(r) {
|
|
1552
2830
|
const key = r.verdict.status + ': ' + (r.reaction || 'adapted');
|
|
1553
2831
|
shiftTracker.shifts[key] = (shiftTracker.shifts[key] || 0) + 1;
|
|
2832
|
+
// Track per-rule impacts for card display
|
|
2833
|
+
var ruleId = r.verdict.ruleId || '';
|
|
2834
|
+
if (ruleId && ruleImpactTracker[ruleId]) {
|
|
2835
|
+
ruleImpactTracker[ruleId].blocks++;
|
|
2836
|
+
} else {
|
|
2837
|
+
// Try to match by pattern — governance may report with NV- prefix
|
|
2838
|
+
Object.keys(ruleImpactTracker).forEach(function(k) {
|
|
2839
|
+
if (ruleId.includes(k) || (r.verdict.reason && r.verdict.reason.toLowerCase().includes(ruleImpactTracker[k].label.toLowerCase()))) {
|
|
2840
|
+
ruleImpactTracker[k].blocks++;
|
|
2841
|
+
}
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
1554
2844
|
// Store raw governed reactions for detail view
|
|
1555
2845
|
shiftTracker.rawGoverned.push({
|
|
1556
2846
|
agent: r.stakeholder_id,
|
|
@@ -1848,6 +3138,480 @@ handleEvent = function(event) {
|
|
|
1848
3138
|
}
|
|
1849
3139
|
};
|
|
1850
3140
|
|
|
3141
|
+
// ============================================
|
|
3142
|
+
// THEME TOGGLE — Light / Dark mode
|
|
3143
|
+
// ============================================
|
|
3144
|
+
const themeToggleBtn = document.getElementById('theme-toggle');
|
|
3145
|
+
function applyTheme(theme) {
|
|
3146
|
+
if (theme === 'light') {
|
|
3147
|
+
document.body.classList.add('light');
|
|
3148
|
+
themeToggleBtn.textContent = 'Dark Mode';
|
|
3149
|
+
} else {
|
|
3150
|
+
document.body.classList.remove('light');
|
|
3151
|
+
themeToggleBtn.textContent = 'Light Mode';
|
|
3152
|
+
}
|
|
3153
|
+
localStorage.setItem('nv-theme', theme);
|
|
3154
|
+
// Update chart colors if chart exists
|
|
3155
|
+
if (chart && chart.options) {
|
|
3156
|
+
const gridColor = theme === 'light' ? '#d4d4d4' : '#2a2a2a';
|
|
3157
|
+
const tickColor = theme === 'light' ? '#666' : '#888';
|
|
3158
|
+
const legendColor = theme === 'light' ? '#444' : '#888';
|
|
3159
|
+
chart.options.scales.x.ticks.color = tickColor;
|
|
3160
|
+
chart.options.scales.x.grid.color = gridColor;
|
|
3161
|
+
chart.options.scales.y.ticks.color = tickColor;
|
|
3162
|
+
chart.options.scales.y.grid.color = gridColor;
|
|
3163
|
+
chart.options.plugins.legend.labels.color = legendColor;
|
|
3164
|
+
chart.update();
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
themeToggleBtn.addEventListener('click', () => {
|
|
3168
|
+
const current = document.body.classList.contains('light') ? 'light' : 'dark';
|
|
3169
|
+
applyTheme(current === 'light' ? 'dark' : 'light');
|
|
3170
|
+
});
|
|
3171
|
+
// Restore saved theme
|
|
3172
|
+
const savedTheme = localStorage.getItem('nv-theme');
|
|
3173
|
+
if (savedTheme) applyTheme(savedTheme);
|
|
3174
|
+
|
|
3175
|
+
// ============================================
|
|
3176
|
+
// WORLD SOURCE SWITCHING
|
|
3177
|
+
// ============================================
|
|
3178
|
+
let currentWorldSource = 'preset';
|
|
3179
|
+
const worldSourceTabs = document.querySelectorAll('.ws-tab');
|
|
3180
|
+
const sourcePresetPanel = document.getElementById('source-preset');
|
|
3181
|
+
const sourceCustomPanel = document.getElementById('source-custom');
|
|
3182
|
+
const sourceUploadPanel = document.getElementById('source-upload');
|
|
3183
|
+
|
|
3184
|
+
worldSourceTabs.forEach(tab => {
|
|
3185
|
+
tab.addEventListener('click', () => {
|
|
3186
|
+
const source = tab.dataset.source;
|
|
3187
|
+
if (source === currentWorldSource) return;
|
|
3188
|
+
|
|
3189
|
+
currentWorldSource = source;
|
|
3190
|
+
|
|
3191
|
+
// Update tab visuals
|
|
3192
|
+
worldSourceTabs.forEach(t => t.classList.remove('active'));
|
|
3193
|
+
tab.classList.add('active');
|
|
3194
|
+
tab.querySelector('input').checked = true;
|
|
3195
|
+
|
|
3196
|
+
// Show/hide panels
|
|
3197
|
+
sourcePresetPanel.style.display = source === 'preset' ? '' : 'none';
|
|
3198
|
+
sourceCustomPanel.style.display = source === 'custom' ? '' : 'none';
|
|
3199
|
+
sourceUploadPanel.style.display = source === 'upload' ? '' : 'none';
|
|
3200
|
+
});
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3203
|
+
// Populate base world selector in custom rules panel
|
|
3204
|
+
function populateBaseWorldSelect() {
|
|
3205
|
+
const select = document.getElementById('custom-base-world');
|
|
3206
|
+
if (!select) return;
|
|
3207
|
+
worlds.forEach(w => {
|
|
3208
|
+
const opt = document.createElement('option');
|
|
3209
|
+
opt.value = w.id;
|
|
3210
|
+
opt.textContent = w.title;
|
|
3211
|
+
select.appendChild(opt);
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// ============================================
|
|
3216
|
+
// WORLD ACTION BAR
|
|
3217
|
+
// ============================================
|
|
3218
|
+
|
|
3219
|
+
// + New World
|
|
3220
|
+
document.getElementById('new-world-btn').addEventListener('click', () => {
|
|
3221
|
+
// Switch to custom rules mode
|
|
3222
|
+
currentWorldSource = 'custom';
|
|
3223
|
+
worldSourceTabs.forEach(t => {
|
|
3224
|
+
t.classList.toggle('active', t.dataset.source === 'custom');
|
|
3225
|
+
t.querySelector('input').checked = t.dataset.source === 'custom';
|
|
3226
|
+
});
|
|
3227
|
+
sourcePresetPanel.style.display = 'none';
|
|
3228
|
+
sourceCustomPanel.style.display = '';
|
|
3229
|
+
sourceUploadPanel.style.display = 'none';
|
|
3230
|
+
|
|
3231
|
+
// Clear everything
|
|
3232
|
+
document.getElementById('custom-world-name').value = '';
|
|
3233
|
+
document.getElementById('custom-world-thesis').value = '';
|
|
3234
|
+
document.getElementById('rule-input').value = '';
|
|
3235
|
+
document.getElementById('parsed-rules').innerHTML = '';
|
|
3236
|
+
document.getElementById('rule-status').textContent = '';
|
|
3237
|
+
document.getElementById('rule-status').className = 'rule-status';
|
|
3238
|
+
document.getElementById('custom-base-world').value = '';
|
|
3239
|
+
|
|
3240
|
+
// Clear active rules server-side
|
|
3241
|
+
fetch('/api/clear-rules', { method: 'POST' });
|
|
3242
|
+
|
|
3243
|
+
// Reset right panel
|
|
3244
|
+
document.getElementById('active-invariants').innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No rules loaded. Define your world.</div>';
|
|
3245
|
+
});
|
|
3246
|
+
|
|
3247
|
+
// Clear Rules
|
|
3248
|
+
document.getElementById('clear-rules-btn').addEventListener('click', async () => {
|
|
3249
|
+
await fetch('/api/clear-rules', { method: 'POST' });
|
|
3250
|
+
|
|
3251
|
+
// Clear rule editor UI
|
|
3252
|
+
const ruleInput = document.getElementById('rule-input');
|
|
3253
|
+
if (ruleInput) ruleInput.value = '';
|
|
3254
|
+
const parsed = document.getElementById('parsed-rules');
|
|
3255
|
+
if (parsed) parsed.innerHTML = '';
|
|
3256
|
+
const status = document.getElementById('rule-status');
|
|
3257
|
+
if (status) { status.textContent = 'Rules cleared.'; status.className = 'rule-status success'; }
|
|
3258
|
+
|
|
3259
|
+
// Clear upload state
|
|
3260
|
+
const uploadStatus = document.getElementById('upload-status');
|
|
3261
|
+
if (uploadStatus) { uploadStatus.textContent = 'Rules cleared.'; uploadStatus.className = 'rule-status success'; }
|
|
3262
|
+
const loadedInfo = document.getElementById('loaded-world-info');
|
|
3263
|
+
if (loadedInfo) loadedInfo.style.display = 'none';
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
// Load World File (switch to upload tab)
|
|
3267
|
+
document.getElementById('load-file-btn').addEventListener('click', () => {
|
|
3268
|
+
currentWorldSource = 'upload';
|
|
3269
|
+
worldSourceTabs.forEach(t => {
|
|
3270
|
+
t.classList.toggle('active', t.dataset.source === 'upload');
|
|
3271
|
+
t.querySelector('input').checked = t.dataset.source === 'upload';
|
|
3272
|
+
});
|
|
3273
|
+
sourcePresetPanel.style.display = 'none';
|
|
3274
|
+
sourceCustomPanel.style.display = 'none';
|
|
3275
|
+
sourceUploadPanel.style.display = '';
|
|
3276
|
+
});
|
|
3277
|
+
|
|
3278
|
+
// Save as World File (export)
|
|
3279
|
+
document.getElementById('export-world-btn').addEventListener('click', async () => {
|
|
3280
|
+
try {
|
|
3281
|
+
const resp = await fetch('/api/export-world');
|
|
3282
|
+
const data = await resp.json();
|
|
3283
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
3284
|
+
const url = URL.createObjectURL(blob);
|
|
3285
|
+
const a = document.createElement('a');
|
|
3286
|
+
a.href = url;
|
|
3287
|
+
a.download = (currentWorld ? currentWorld.id : 'custom') + '-world.json';
|
|
3288
|
+
document.body.appendChild(a);
|
|
3289
|
+
a.click();
|
|
3290
|
+
document.body.removeChild(a);
|
|
3291
|
+
URL.revokeObjectURL(url);
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
alert('Export failed: ' + err.message);
|
|
3294
|
+
}
|
|
3295
|
+
});
|
|
3296
|
+
|
|
3297
|
+
// ============================================
|
|
3298
|
+
// WORLD FILE UPLOAD / PASTE
|
|
3299
|
+
// ============================================
|
|
3300
|
+
const uploadZone = document.getElementById('upload-zone');
|
|
3301
|
+
const uploadFileInput = document.getElementById('upload-file-input');
|
|
3302
|
+
const uploadBrowseBtn = document.getElementById('upload-browse-btn');
|
|
3303
|
+
const worldJsonInput = document.getElementById('world-json-input');
|
|
3304
|
+
const loadWorldBtn = document.getElementById('load-world-btn');
|
|
3305
|
+
const uploadStatusEl = document.getElementById('upload-status');
|
|
3306
|
+
|
|
3307
|
+
// Drag and drop
|
|
3308
|
+
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
|
|
3309
|
+
uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
|
|
3310
|
+
uploadZone.addEventListener('drop', (e) => {
|
|
3311
|
+
e.preventDefault();
|
|
3312
|
+
uploadZone.classList.remove('dragover');
|
|
3313
|
+
const file = e.dataTransfer.files[0];
|
|
3314
|
+
if (file) readWorldFile(file);
|
|
3315
|
+
});
|
|
3316
|
+
|
|
3317
|
+
// Browse button
|
|
3318
|
+
uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadFileInput.click(); });
|
|
3319
|
+
uploadFileInput.addEventListener('change', () => {
|
|
3320
|
+
if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]);
|
|
3321
|
+
});
|
|
3322
|
+
|
|
3323
|
+
// Click zone to browse
|
|
3324
|
+
uploadZone.addEventListener('click', () => { uploadFileInput.click(); });
|
|
3325
|
+
|
|
3326
|
+
function readWorldFile(file) {
|
|
3327
|
+
const reader = new FileReader();
|
|
3328
|
+
reader.onload = (e) => {
|
|
3329
|
+
worldJsonInput.value = e.target.result;
|
|
3330
|
+
uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".';
|
|
3331
|
+
uploadStatusEl.className = 'rule-status success';
|
|
3332
|
+
};
|
|
3333
|
+
reader.readAsText(file);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// Load into Runtime
|
|
3337
|
+
loadWorldBtn.addEventListener('click', async () => {
|
|
3338
|
+
const jsonText = worldJsonInput.value.trim();
|
|
3339
|
+
if (!jsonText) {
|
|
3340
|
+
uploadStatusEl.textContent = 'Paste or upload a world file first.';
|
|
3341
|
+
uploadStatusEl.className = 'rule-status error';
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
let worldData;
|
|
3346
|
+
try {
|
|
3347
|
+
worldData = JSON.parse(jsonText);
|
|
3348
|
+
} catch (err) {
|
|
3349
|
+
uploadStatusEl.textContent = 'Invalid JSON: ' + err.message;
|
|
3350
|
+
uploadStatusEl.className = 'rule-status error';
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
// Normalize: if the JSON is { world: {...} } or just {...}
|
|
3355
|
+
const worldPayload = worldData.world || worldData;
|
|
3356
|
+
|
|
3357
|
+
loadWorldBtn.textContent = 'Loading...';
|
|
3358
|
+
loadWorldBtn.disabled = true;
|
|
3359
|
+
|
|
3360
|
+
try {
|
|
3361
|
+
const resp = await fetch('/api/load-world-file', {
|
|
3362
|
+
method: 'POST',
|
|
3363
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3364
|
+
body: JSON.stringify({ world: worldPayload }),
|
|
3365
|
+
});
|
|
3366
|
+
const result = await resp.json();
|
|
3367
|
+
|
|
3368
|
+
if (result.error) {
|
|
3369
|
+
uploadStatusEl.textContent = result.error;
|
|
3370
|
+
uploadStatusEl.className = 'rule-status error';
|
|
3371
|
+
} else {
|
|
3372
|
+
uploadStatusEl.textContent = result.message;
|
|
3373
|
+
uploadStatusEl.className = 'rule-status success';
|
|
3374
|
+
|
|
3375
|
+
// Show loaded world info
|
|
3376
|
+
const infoEl = document.getElementById('loaded-world-info');
|
|
3377
|
+
infoEl.style.display = '';
|
|
3378
|
+
document.getElementById('lw-name').textContent = result.world.title;
|
|
3379
|
+
document.getElementById('lw-thesis').textContent = '"' + result.world.thesis + '"';
|
|
3380
|
+
document.getElementById('lw-stats').textContent =
|
|
3381
|
+
result.world.invariants.length + ' invariants, ' +
|
|
3382
|
+
result.world.gates.length + ' gates, ' +
|
|
3383
|
+
result.rulesApplied + ' rules';
|
|
3384
|
+
|
|
3385
|
+
// Update active invariants in right panel
|
|
3386
|
+
const invHtml = result.world.invariants.map(inv =>
|
|
3387
|
+
'<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
|
|
3388
|
+
).join('') + result.world.gates.map(g =>
|
|
3389
|
+
'<div class="inv-item" style="color:' + (g.severity === 'critical' ? 'var(--red)' : 'var(--yellow)') + '">[' + g.id + '] ' + g.label + '</div>'
|
|
3390
|
+
).join('');
|
|
3391
|
+
activeInvEl.innerHTML = invHtml || '<div style="font-size:11px;color:var(--text-muted)">No invariants defined</div>';
|
|
3392
|
+
|
|
3393
|
+
// Update state variables if present
|
|
3394
|
+
if (result.world.stateVariables && result.world.stateVariables.length > 0) {
|
|
3395
|
+
// Store as a pseudo-world so sliders render
|
|
3396
|
+
currentWorld = {
|
|
3397
|
+
id: 'custom-world',
|
|
3398
|
+
title: result.world.title,
|
|
3399
|
+
thesis: result.world.thesis,
|
|
3400
|
+
stateVariables: result.world.stateVariables,
|
|
3401
|
+
invariants: result.world.invariants,
|
|
3402
|
+
gates: result.world.gates,
|
|
3403
|
+
};
|
|
3404
|
+
selectWorld('custom-world');
|
|
3405
|
+
} else {
|
|
3406
|
+
// Just set current world reference
|
|
3407
|
+
currentWorld = {
|
|
3408
|
+
id: 'custom-world',
|
|
3409
|
+
title: result.world.title,
|
|
3410
|
+
thesis: result.world.thesis,
|
|
3411
|
+
stateVariables: [],
|
|
3412
|
+
invariants: result.world.invariants,
|
|
3413
|
+
gates: result.world.gates,
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
} catch (err) {
|
|
3418
|
+
uploadStatusEl.textContent = 'Error: ' + err.message;
|
|
3419
|
+
uploadStatusEl.className = 'rule-status error';
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
loadWorldBtn.textContent = 'Load into Runtime';
|
|
3423
|
+
loadWorldBtn.disabled = false;
|
|
3424
|
+
});
|
|
3425
|
+
|
|
3426
|
+
// ============================================
|
|
3427
|
+
// PLAIN-ENGLISH RULE EDITOR
|
|
3428
|
+
// ============================================
|
|
3429
|
+
const ruleInput = document.getElementById('rule-input');
|
|
3430
|
+
const parseRulesBtn = document.getElementById('parse-rules-btn');
|
|
3431
|
+
const parsedRulesEl = document.getElementById('parsed-rules');
|
|
3432
|
+
const ruleStatusEl = document.getElementById('rule-status');
|
|
3433
|
+
let parsedRuleData = [];
|
|
3434
|
+
|
|
3435
|
+
parseRulesBtn.addEventListener('click', async () => {
|
|
3436
|
+
const text = ruleInput.value.trim();
|
|
3437
|
+
if (!text) return;
|
|
3438
|
+
|
|
3439
|
+
parseRulesBtn.disabled = true;
|
|
3440
|
+
parseRulesBtn.textContent = 'Parsing...';
|
|
3441
|
+
ruleStatusEl.textContent = '';
|
|
3442
|
+
ruleStatusEl.className = 'rule-status';
|
|
3443
|
+
|
|
3444
|
+
try {
|
|
3445
|
+
const resp = await fetch('/api/parse-rules', {
|
|
3446
|
+
method: 'POST',
|
|
3447
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3448
|
+
body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
|
|
3449
|
+
});
|
|
3450
|
+
const data = await resp.json();
|
|
3451
|
+
|
|
3452
|
+
if (data.error) {
|
|
3453
|
+
ruleStatusEl.textContent = data.error;
|
|
3454
|
+
ruleStatusEl.className = 'rule-status error';
|
|
3455
|
+
parsedRulesEl.innerHTML = '';
|
|
3456
|
+
parsedRuleData = [];
|
|
3457
|
+
} else {
|
|
3458
|
+
parsedRuleData = data.rules || [];
|
|
3459
|
+
parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
|
|
3460
|
+
const enfType = r.enforcement || 'block';
|
|
3461
|
+
const iconMap = { block: '🔴', allow: '🟢', modify: '🔵', warn: '🟡', pause: '🟡' };
|
|
3462
|
+
const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
|
|
3463
|
+
const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
|
|
3464
|
+
const icon = iconMap[enfType] || '🟢';
|
|
3465
|
+
const label = labelMap[enfType] || 'Rule';
|
|
3466
|
+
const effect = effectMap[enfType] || 'Active';
|
|
3467
|
+
return '<div class="parsed-rule enforcement-' + enfType + '">' +
|
|
3468
|
+
'<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
|
|
3469
|
+
'<div class="pr-desc">' + r.description + '</div>' +
|
|
3470
|
+
'<div class="pr-patterns">' + effect + ' • Matches: ' + r.intent_patterns.join(', ') + '</div>' +
|
|
3471
|
+
'</div>';
|
|
3472
|
+
}).join('');
|
|
3473
|
+
|
|
3474
|
+
if (parsedRuleData.length > 0) {
|
|
3475
|
+
const btnLabel = currentWorldSource === 'custom' ? 'Generate World with ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') : 'Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation';
|
|
3476
|
+
parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">' + btnLabel + '</button>';
|
|
3477
|
+
document.getElementById('apply-rules-btn').addEventListener('click', async () => {
|
|
3478
|
+
try {
|
|
3479
|
+
// If in custom rules mode, use base world if selected
|
|
3480
|
+
let worldId = currentWorld ? currentWorld.id : 'trading';
|
|
3481
|
+
if (currentWorldSource === 'custom') {
|
|
3482
|
+
const baseWorld = document.getElementById('custom-base-world').value;
|
|
3483
|
+
if (baseWorld) worldId = baseWorld;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
const applyResp = await fetch('/api/apply-rules', {
|
|
3487
|
+
method: 'POST',
|
|
3488
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3489
|
+
body: JSON.stringify({ rules: parsedRuleData, worldId }),
|
|
3490
|
+
});
|
|
3491
|
+
const applyData = await applyResp.json();
|
|
3492
|
+
if (applyData.status === 'applied') {
|
|
3493
|
+
ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
|
|
3494
|
+
ruleStatusEl.className = 'rule-status success';
|
|
3495
|
+
|
|
3496
|
+
// Update right panel invariants with custom rules
|
|
3497
|
+
const customName = document.getElementById('custom-world-name');
|
|
3498
|
+
const worldName = (customName && customName.value) ? customName.value : 'Custom World';
|
|
3499
|
+
const customThesis = document.getElementById('custom-world-thesis');
|
|
3500
|
+
const thesis = (customThesis && customThesis.value) ? customThesis.value : 'User-defined governance rules';
|
|
3501
|
+
|
|
3502
|
+
// Show rules in active invariants panel
|
|
3503
|
+
activeInvEl.innerHTML = parsedRuleData.map(r => {
|
|
3504
|
+
const enfType = r.enforcement || 'block';
|
|
3505
|
+
const colorMap = { block: 'var(--red)', allow: 'var(--green)', modify: 'var(--blue)', warn: 'var(--yellow)', pause: 'var(--yellow)' };
|
|
3506
|
+
const color = colorMap[enfType] || 'var(--text-secondary)';
|
|
3507
|
+
return '<div class="inv-item" style="color:' + color + '">[' + r.id + '] ' + r.description + '</div>';
|
|
3508
|
+
}).join('');
|
|
3509
|
+
|
|
3510
|
+
// In custom mode, set a custom world reference
|
|
3511
|
+
if (currentWorldSource === 'custom') {
|
|
3512
|
+
const baseWorld = document.getElementById('custom-base-world').value;
|
|
3513
|
+
if (baseWorld) {
|
|
3514
|
+
selectWorld(baseWorld);
|
|
3515
|
+
} else {
|
|
3516
|
+
currentWorld = { id: 'custom-world', title: worldName, thesis, stateVariables: [], invariants: [], gates: [] };
|
|
3517
|
+
}
|
|
3518
|
+
document.getElementById('world-thesis').textContent = '"' + thesis + '"';
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
} catch (err) {
|
|
3522
|
+
ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
|
|
3523
|
+
ruleStatusEl.className = 'rule-status error';
|
|
3524
|
+
}
|
|
3525
|
+
});
|
|
3526
|
+
ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
|
|
3527
|
+
ruleStatusEl.className = 'rule-status success';
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
} catch (err) {
|
|
3531
|
+
ruleStatusEl.textContent = 'Error: ' + err.message;
|
|
3532
|
+
ruleStatusEl.className = 'rule-status error';
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
parseRulesBtn.disabled = false;
|
|
3536
|
+
parseRulesBtn.textContent = 'Parse Rules';
|
|
3537
|
+
});
|
|
3538
|
+
|
|
3539
|
+
// ============================================
|
|
3540
|
+
// SESSION TRACKING
|
|
3541
|
+
// ============================================
|
|
3542
|
+
|
|
3543
|
+
let sessionPollInterval = null;
|
|
3544
|
+
|
|
3545
|
+
async function pollSessionStats() {
|
|
3546
|
+
try {
|
|
3547
|
+
const resp = await fetch('/api/session');
|
|
3548
|
+
const data = await resp.json();
|
|
3549
|
+
const el = (id) => document.getElementById(id);
|
|
3550
|
+
if (el('s-total')) el('s-total').textContent = data.evaluations.total;
|
|
3551
|
+
if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
|
|
3552
|
+
if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
|
|
3553
|
+
if (el('s-allowed')) el('s-allowed').textContent = data.evaluations.allowed;
|
|
3554
|
+
if (el('s-agents')) {
|
|
3555
|
+
el('s-agents').textContent = data.agents.length > 0
|
|
3556
|
+
? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
|
|
3557
|
+
: 'No agents connected yet';
|
|
3558
|
+
}
|
|
3559
|
+
if (el('session-history') && data.historyCount > 0) {
|
|
3560
|
+
el('session-history').textContent = data.historyCount + ' previous session(s) saved for comparison';
|
|
3561
|
+
}
|
|
3562
|
+
} catch {}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
async function viewSessionReport() {
|
|
3566
|
+
try {
|
|
3567
|
+
const resp = await fetch('/api/session/report');
|
|
3568
|
+
const text = await resp.text();
|
|
3569
|
+
const log = document.getElementById('sim-log');
|
|
3570
|
+
if (log) {
|
|
3571
|
+
const div = document.createElement('div');
|
|
3572
|
+
div.className = 'log-round';
|
|
3573
|
+
div.innerHTML = '<h4 style="color:#818cf8">Enforcement Report</h4><pre style="white-space:pre-wrap;font-size:11px;color:#d4d4d8">' + text.replace(/</g,'<') + '</pre>';
|
|
3574
|
+
log.prepend(div);
|
|
3575
|
+
log.scrollTop = 0;
|
|
3576
|
+
}
|
|
3577
|
+
} catch (err) { console.error('Failed to load report', err); }
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
async function resetSession() {
|
|
3581
|
+
if (!confirm('Reset session? Current data will be saved for comparison.')) return;
|
|
3582
|
+
try {
|
|
3583
|
+
const resp = await fetch('/api/session/reset', { method: 'POST' });
|
|
3584
|
+
const data = await resp.json();
|
|
3585
|
+
const log = document.getElementById('sim-log');
|
|
3586
|
+
if (log) {
|
|
3587
|
+
const div = document.createElement('div');
|
|
3588
|
+
div.className = 'log-round';
|
|
3589
|
+
div.innerHTML = '<h4 style="color:#fbbf24">Session Reset</h4><div style="font-size:11px;color:#a8a29e">' + data.message + '</div>';
|
|
3590
|
+
log.prepend(div);
|
|
3591
|
+
}
|
|
3592
|
+
pollSessionStats();
|
|
3593
|
+
} catch (err) { console.error('Failed to reset session', err); }
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
async function saveExperiment() {
|
|
3597
|
+
try {
|
|
3598
|
+
const resp = await fetch('/api/session/save', { method: 'POST' });
|
|
3599
|
+
const data = await resp.json();
|
|
3600
|
+
if (data.error) { alert(data.error); return; }
|
|
3601
|
+
const log = document.getElementById('sim-log');
|
|
3602
|
+
if (log) {
|
|
3603
|
+
const div = document.createElement('div');
|
|
3604
|
+
div.className = 'log-round';
|
|
3605
|
+
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>';
|
|
3606
|
+
log.prepend(div);
|
|
3607
|
+
}
|
|
3608
|
+
} catch (err) { console.error('Failed to save experiment', err); }
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// Poll session stats every 2 seconds
|
|
3612
|
+
sessionPollInterval = setInterval(pollSessionStats, 2000);
|
|
3613
|
+
pollSessionStats();
|
|
3614
|
+
|
|
1851
3615
|
// ============================================
|
|
1852
3616
|
// BOOT
|
|
1853
3617
|
// ============================================
|