@neuroverseos/nv-sim 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- 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 +1381 -175
- 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,274 @@ 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
|
+
// ── Session Reporting Endpoints ──
|
|
962
|
+
// Connect the serve runtime to the enforce reporting pipeline.
|
|
963
|
+
// Users can request reports, stats, and recommendations from live governance data.
|
|
964
|
+
// GET /api/session — current session stats (lightweight)
|
|
965
|
+
if (req.url === "/api/session" && req.method === "GET") {
|
|
966
|
+
const evals = currentSession.evaluations;
|
|
967
|
+
const blocked = evals.filter(e => e.decision === "BLOCK").length;
|
|
968
|
+
const modified = evals.filter(e => e.decision === "MODIFY").length;
|
|
969
|
+
const allowed = evals.filter(e => e.decision === "ALLOW").length;
|
|
970
|
+
const uniqueActors = [...new Set(evals.map(e => e.actor))];
|
|
971
|
+
const uniqueActions = [...new Set(evals.map(e => e.action))];
|
|
972
|
+
const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
|
|
973
|
+
jsonResponse(res, 200, {
|
|
974
|
+
sessionId: currentSession.id,
|
|
975
|
+
startedAt: currentSession.startedAt,
|
|
976
|
+
world: currentSession.world,
|
|
977
|
+
guardCount: currentSession.guardCount,
|
|
978
|
+
evaluations: {
|
|
979
|
+
total: evals.length,
|
|
980
|
+
blocked,
|
|
981
|
+
modified,
|
|
982
|
+
allowed,
|
|
983
|
+
},
|
|
984
|
+
agents: uniqueActors,
|
|
985
|
+
actionTypes: uniqueActions,
|
|
986
|
+
triggeredRules,
|
|
987
|
+
historyCount: sessionHistory.length,
|
|
988
|
+
});
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
// GET /api/session/report — full enforcement report (text)
|
|
992
|
+
if (req.url === "/api/session/report" && req.method === "GET") {
|
|
993
|
+
const report = synthesizeSessionReport();
|
|
994
|
+
// Format as human-readable text using the same style as enforce CLI
|
|
995
|
+
const lines = [];
|
|
996
|
+
lines.push("");
|
|
997
|
+
lines.push(" LIVE GOVERNANCE — ENFORCEMENT REPORT");
|
|
998
|
+
lines.push(" " + "=".repeat(70));
|
|
999
|
+
lines.push(` Session: ${report.sessionId}`);
|
|
1000
|
+
lines.push(` Scenario: ${report.scenario}`);
|
|
1001
|
+
lines.push(` Sessions: ${report.runs.length}`);
|
|
1002
|
+
lines.push(` Generated: ${report.generatedAt}`);
|
|
1003
|
+
lines.push("");
|
|
1004
|
+
if (report.runs.length > 0) {
|
|
1005
|
+
lines.push(" RUN HISTORY");
|
|
1006
|
+
lines.push(" " + "-".repeat(70));
|
|
1007
|
+
lines.push(` ${"#".padEnd(4)} ${"World".padEnd(25)} ${"Rules".padEnd(8)} ${"Evals".padEnd(8)} ${"Blocked".padEnd(10)} ${"Effectiveness"}`);
|
|
1008
|
+
lines.push(" " + "-".repeat(70));
|
|
1009
|
+
for (const run of report.runs) {
|
|
1010
|
+
const name = run.worldName.length > 23 ? run.worldName.slice(0, 22) + "…" : run.worldName;
|
|
1011
|
+
lines.push(` ${String(run.iteration).padEnd(4)} ${name.padEnd(25)} ${String(run.ruleCount).padEnd(8)} ${String(run.governanceStats.totalEvaluations).padEnd(8)} ${String(run.governanceStats.verdicts.block).padEnd(10)} ${(run.comparison.governanceEffectiveness * 100).toFixed(0)}%`);
|
|
1012
|
+
}
|
|
1013
|
+
lines.push("");
|
|
1014
|
+
lines.push(" DIVERGENCE ANALYSIS");
|
|
1015
|
+
lines.push(" " + "-".repeat(70));
|
|
1016
|
+
lines.push(` Stability trend: ${report.divergence.stabilityTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1017
|
+
lines.push(` Collapse trend: ${report.divergence.collapseTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1018
|
+
lines.push(` Effectiveness trend: ${report.divergence.effectivenessTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1019
|
+
lines.push(` Total divergence: ${report.divergence.totalDivergence}`);
|
|
1020
|
+
lines.push("");
|
|
1021
|
+
lines.push(` ${report.divergence.narrative}`);
|
|
1022
|
+
// Blocked action breakdown
|
|
1023
|
+
const allEvals = [
|
|
1024
|
+
...sessionHistory.flatMap(s => s.evaluations),
|
|
1025
|
+
...currentSession.evaluations,
|
|
1026
|
+
];
|
|
1027
|
+
const blockedEvals = allEvals.filter(e => e.decision === "BLOCK");
|
|
1028
|
+
if (blockedEvals.length > 0) {
|
|
1029
|
+
lines.push("");
|
|
1030
|
+
lines.push(" BLOCKED ACTIONS");
|
|
1031
|
+
lines.push(" " + "-".repeat(70));
|
|
1032
|
+
// Group by rule
|
|
1033
|
+
const byRule = {};
|
|
1034
|
+
for (const e of blockedEvals) {
|
|
1035
|
+
const key = e.ruleId ?? "unknown";
|
|
1036
|
+
if (!byRule[key])
|
|
1037
|
+
byRule[key] = { count: 0, actors: new Set(), actions: new Set() };
|
|
1038
|
+
byRule[key].count++;
|
|
1039
|
+
byRule[key].actors.add(e.actor);
|
|
1040
|
+
byRule[key].actions.add(e.action);
|
|
1041
|
+
}
|
|
1042
|
+
const sorted = Object.entries(byRule).sort((a, b) => b[1].count - a[1].count);
|
|
1043
|
+
for (const [ruleId, data] of sorted) {
|
|
1044
|
+
lines.push(` ${ruleId}: ${data.count} blocks across ${data.actors.size} agent(s)`);
|
|
1045
|
+
lines.push(` Actions: ${[...data.actions].join(", ")}`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// Recommendations (deterministic, no LLM needed)
|
|
1049
|
+
lines.push("");
|
|
1050
|
+
lines.push(" RECOMMENDATIONS");
|
|
1051
|
+
lines.push(" " + "-".repeat(70));
|
|
1052
|
+
const best = report.runs[report.divergence.bestIteration - 1];
|
|
1053
|
+
if (best) {
|
|
1054
|
+
const totalEvals = best.governanceStats.totalEvaluations;
|
|
1055
|
+
const blockRate = totalEvals > 0 ? best.governanceStats.verdicts.block / totalEvals : 0;
|
|
1056
|
+
if (blockRate > 0.5) {
|
|
1057
|
+
lines.push(" HIGH BLOCK RATE — Rules may be too aggressive.");
|
|
1058
|
+
lines.push(" Try relaxing constraints or adding MODIFY actions instead of hard blocks.");
|
|
1059
|
+
lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
|
|
1060
|
+
}
|
|
1061
|
+
else if (blockRate < 0.05 && totalEvals > 20) {
|
|
1062
|
+
lines.push(" LOW BLOCK RATE — Rules may be too permissive.");
|
|
1063
|
+
lines.push(" Try adding content-specific guards or lowering thresholds.");
|
|
1064
|
+
lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
|
|
1065
|
+
}
|
|
1066
|
+
else if (blockRate > 0) {
|
|
1067
|
+
lines.push(` Governance is active: ${(blockRate * 100).toFixed(0)}% of actions blocked.`);
|
|
1068
|
+
lines.push(" To iterate: apply different rules via POST /api/apply-rules, then POST /api/session/reset.");
|
|
1069
|
+
lines.push(" This creates a new session for side-by-side comparison.");
|
|
1070
|
+
}
|
|
1071
|
+
if (report.runs.length === 1) {
|
|
1072
|
+
lines.push("");
|
|
1073
|
+
lines.push(" EXPERIMENT: Apply different rules and reset to compare sessions.");
|
|
1074
|
+
lines.push(" This will show divergence: how different governance changes outcomes.");
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
lines.push("");
|
|
1078
|
+
lines.push(" RECOMMENDATION");
|
|
1079
|
+
lines.push(" " + "-".repeat(70));
|
|
1080
|
+
lines.push(` ${report.recommendation}`);
|
|
1081
|
+
}
|
|
1082
|
+
lines.push("");
|
|
1083
|
+
lines.push(" " + "=".repeat(70));
|
|
1084
|
+
lines.push(" NeuroVerse Policy Enforcement System — Live Governance");
|
|
1085
|
+
lines.push(" Design rules. Run reality. See what changes.");
|
|
1086
|
+
lines.push(" " + "=".repeat(70));
|
|
1087
|
+
lines.push("");
|
|
1088
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
1089
|
+
res.end(lines.join("\n"));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
// GET /api/session/report.json — full enforcement report (JSON)
|
|
1093
|
+
if (req.url === "/api/session/report.json" && req.method === "GET") {
|
|
1094
|
+
const report = synthesizeSessionReport();
|
|
1095
|
+
jsonResponse(res, 200, report);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
// GET /api/session/evaluations — raw evaluation log
|
|
1099
|
+
if (req.url === "/api/session/evaluations" && req.method === "GET") {
|
|
1100
|
+
jsonResponse(res, 200, {
|
|
1101
|
+
sessionId: currentSession.id,
|
|
1102
|
+
count: currentSession.evaluations.length,
|
|
1103
|
+
evaluations: currentSession.evaluations.slice(-200), // last 200
|
|
1104
|
+
});
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
// POST /api/session/reset — snapshot current session and start fresh
|
|
1108
|
+
// This is how users create multi-run comparisons:
|
|
1109
|
+
// 1. Apply rules A → run simulation → POST /api/session/reset
|
|
1110
|
+
// 2. Apply rules B → run simulation → GET /api/session/report
|
|
1111
|
+
// Now the report shows divergence between rules A and rules B.
|
|
1112
|
+
if (req.url === "/api/session/reset" && req.method === "POST") {
|
|
1113
|
+
if (currentSession.evaluations.length > 0) {
|
|
1114
|
+
sessionHistory.push({ ...currentSession, evaluations: [...currentSession.evaluations] });
|
|
1115
|
+
}
|
|
1116
|
+
const body = await readBody(req).catch(() => "{}");
|
|
1117
|
+
const opts = JSON.parse(body || "{}");
|
|
1118
|
+
currentSession = {
|
|
1119
|
+
id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1120
|
+
startedAt: new Date().toISOString(),
|
|
1121
|
+
world: opts.world ?? currentSession.world,
|
|
1122
|
+
guardCount: customGuards.length,
|
|
1123
|
+
evaluations: [],
|
|
1124
|
+
};
|
|
1125
|
+
jsonResponse(res, 200, {
|
|
1126
|
+
status: "reset",
|
|
1127
|
+
newSessionId: currentSession.id,
|
|
1128
|
+
previousSessions: sessionHistory.length,
|
|
1129
|
+
message: `Session reset. ${sessionHistory.length} session(s) in history. Apply new rules and evaluate to compare.`,
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
// POST /api/session/save — save session as experiment with lineage
|
|
1134
|
+
if (req.url === "/api/session/save" && req.method === "POST") {
|
|
1135
|
+
const report = synthesizeSessionReport();
|
|
1136
|
+
if (report.runs.length === 0) {
|
|
1137
|
+
jsonResponse(res, 400, { error: "No evaluations to save. Send actions to /api/evaluate first." });
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const experiment = {
|
|
1141
|
+
id: `exp-live-${Date.now().toString(36)}`,
|
|
1142
|
+
savedAt: new Date().toISOString(),
|
|
1143
|
+
scenario: report.scenario,
|
|
1144
|
+
source: "live-governance",
|
|
1145
|
+
sessions: report.runs.length,
|
|
1146
|
+
totalEvaluations: report.runs.reduce((sum, r) => sum + r.governanceStats.totalEvaluations, 0),
|
|
1147
|
+
metrics: {
|
|
1148
|
+
stability: report.runs[report.divergence.bestIteration - 1]?.metrics.stabilityScore ?? 0,
|
|
1149
|
+
effectiveness: report.runs[report.divergence.bestIteration - 1]?.comparison.governanceEffectiveness ?? 0,
|
|
1150
|
+
collapseProbability: report.runs[report.divergence.bestIteration - 1]?.metrics.collapseProbability ?? 0,
|
|
1151
|
+
},
|
|
1152
|
+
divergence: report.divergence,
|
|
1153
|
+
recommendation: report.recommendation,
|
|
1154
|
+
report,
|
|
1155
|
+
};
|
|
1156
|
+
// Save to experiments/ directory
|
|
1157
|
+
try {
|
|
1158
|
+
if (!fs.existsSync("experiments"))
|
|
1159
|
+
fs.mkdirSync("experiments", { recursive: true });
|
|
1160
|
+
const filePath = path.join("experiments", `${experiment.id}.json`);
|
|
1161
|
+
fs.writeFileSync(filePath, JSON.stringify(experiment, null, 2));
|
|
1162
|
+
jsonResponse(res, 200, {
|
|
1163
|
+
status: "saved",
|
|
1164
|
+
experimentId: experiment.id,
|
|
1165
|
+
filePath,
|
|
1166
|
+
metrics: experiment.metrics,
|
|
1167
|
+
message: `Saved to ${filePath}. View with: npx nv-sim enforce --load ${filePath}`,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
catch (err) {
|
|
1171
|
+
jsonResponse(res, 500, { error: "Failed to save experiment", detail: String(err) });
|
|
1172
|
+
}
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
492
1175
|
// List available live adapters
|
|
493
1176
|
if (req.url === "/api/adapters" && req.method === "GET") {
|
|
494
1177
|
const adapters = Object.values(liveAdapter_1.ADAPTER_REGISTRY).map(a => ({
|
|
@@ -616,6 +1299,81 @@ function startInteractiveServer(port, onReady) {
|
|
|
616
1299
|
}
|
|
617
1300
|
return;
|
|
618
1301
|
}
|
|
1302
|
+
// ── AUDIT TRAIL ENDPOINTS ──
|
|
1303
|
+
// GET /api/audit — summary of current session's audit trail
|
|
1304
|
+
if (req.url === "/api/audit" && req.method === "GET") {
|
|
1305
|
+
jsonResponse(res, 200, auditTrail.summary());
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
// GET /api/audit/entries — all entries from current session (with optional filters)
|
|
1309
|
+
if (req.url?.startsWith("/api/audit/entries") && req.method === "GET") {
|
|
1310
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1311
|
+
const entries = auditTrail.query({
|
|
1312
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1313
|
+
agent: url.searchParams.get("agent") ?? undefined,
|
|
1314
|
+
verdict: url.searchParams.get("verdict") ?? undefined,
|
|
1315
|
+
runId: url.searchParams.get("runId") ?? undefined,
|
|
1316
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
1317
|
+
before: url.searchParams.get("before") ?? undefined,
|
|
1318
|
+
});
|
|
1319
|
+
jsonResponse(res, 200, {
|
|
1320
|
+
sessionId: auditTrail.getSessionId(),
|
|
1321
|
+
count: entries.length,
|
|
1322
|
+
entries,
|
|
1323
|
+
});
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
// GET /api/audit/entries/text — human-readable audit trail
|
|
1327
|
+
if (req.url?.startsWith("/api/audit/entries/text") && req.method === "GET") {
|
|
1328
|
+
const entries = auditTrail.readAll();
|
|
1329
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
1330
|
+
res.end((0, auditTrace_1.formatAuditTrail)(entries, { verbose: true }));
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
// GET /api/audit/sessions — list all audit sessions on disk
|
|
1334
|
+
if (req.url === "/api/audit/sessions" && req.method === "GET") {
|
|
1335
|
+
const sessions = (0, auditTrace_1.listAuditSessions)();
|
|
1336
|
+
jsonResponse(res, 200, {
|
|
1337
|
+
sessions: sessions.map((id) => {
|
|
1338
|
+
const trail = (0, auditTrace_1.loadAuditSession)(id);
|
|
1339
|
+
return trail.summary();
|
|
1340
|
+
}),
|
|
1341
|
+
});
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
// GET /api/audit/sessions/:id — load a specific session's audit trail
|
|
1345
|
+
const auditSessionMatch = req.url?.match(/^\/api\/audit\/sessions\/([^/]+)$/);
|
|
1346
|
+
if (auditSessionMatch && req.method === "GET") {
|
|
1347
|
+
const sessionId = decodeURIComponent(auditSessionMatch[1]);
|
|
1348
|
+
const trail = (0, auditTrace_1.loadAuditSession)(sessionId);
|
|
1349
|
+
const entries = trail.readAll();
|
|
1350
|
+
if (entries.length === 0) {
|
|
1351
|
+
jsonResponse(res, 404, { error: `Audit session "${sessionId}" not found or empty` });
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
jsonResponse(res, 200, {
|
|
1355
|
+
...trail.summary(),
|
|
1356
|
+
entries,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
// GET /api/audit/search — search across ALL sessions
|
|
1362
|
+
if (req.url?.startsWith("/api/audit/search") && req.method === "GET") {
|
|
1363
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1364
|
+
const results = (0, auditTrace_1.searchAuditTrails)({
|
|
1365
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1366
|
+
agent: url.searchParams.get("agent") ?? undefined,
|
|
1367
|
+
verdict: url.searchParams.get("verdict") ?? undefined,
|
|
1368
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
1369
|
+
before: url.searchParams.get("before") ?? undefined,
|
|
1370
|
+
});
|
|
1371
|
+
jsonResponse(res, 200, {
|
|
1372
|
+
count: results.length,
|
|
1373
|
+
entries: results,
|
|
1374
|
+
});
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
619
1377
|
// Serve the interactive dashboard
|
|
620
1378
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
621
1379
|
res.end(INTERACTIVE_DASHBOARD_HTML);
|
|
@@ -632,207 +1390,303 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
632
1390
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
633
1391
|
<title>NV-SIM — Scenario Control Platform</title>
|
|
634
1392
|
<style>
|
|
1393
|
+
/* ── Theme Variables ── */
|
|
1394
|
+
:root {
|
|
1395
|
+
--bg-primary: #0f0f0f;
|
|
1396
|
+
--bg-secondary: #141414;
|
|
1397
|
+
--bg-surface: #1a1a1a;
|
|
1398
|
+
--bg-elevated: #222;
|
|
1399
|
+
--border: #2a2a2a;
|
|
1400
|
+
--border-subtle: #333;
|
|
1401
|
+
--text-primary: #f0f0f0;
|
|
1402
|
+
--text-secondary: #b0b0b0;
|
|
1403
|
+
--text-muted: #888;
|
|
1404
|
+
--text-faint: #666;
|
|
1405
|
+
--accent: #818cf8;
|
|
1406
|
+
--accent-bg: #1e1e3a;
|
|
1407
|
+
--green: #4ade80;
|
|
1408
|
+
--green-bg: #0a2a14;
|
|
1409
|
+
--red: #f87171;
|
|
1410
|
+
--red-bg: #2d0a0a;
|
|
1411
|
+
--yellow: #fbbf24;
|
|
1412
|
+
--yellow-bg: #2d2006;
|
|
1413
|
+
--blue: #60a5fa;
|
|
1414
|
+
--blue-bg: #1e293b;
|
|
1415
|
+
--purple: #a78bfa;
|
|
1416
|
+
}
|
|
1417
|
+
body.light {
|
|
1418
|
+
--bg-primary: #f5f5f5;
|
|
1419
|
+
--bg-secondary: #eaeaea;
|
|
1420
|
+
--bg-surface: #e0e0e0;
|
|
1421
|
+
--bg-elevated: #d4d4d4;
|
|
1422
|
+
--border: #c0c0c0;
|
|
1423
|
+
--border-subtle: #b0b0b0;
|
|
1424
|
+
--text-primary: #1a1a1a;
|
|
1425
|
+
--text-secondary: #444;
|
|
1426
|
+
--text-muted: #666;
|
|
1427
|
+
--text-faint: #888;
|
|
1428
|
+
--accent: #6366f1;
|
|
1429
|
+
--accent-bg: #e8e8ff;
|
|
1430
|
+
--green: #16a34a;
|
|
1431
|
+
--green-bg: #dcfce7;
|
|
1432
|
+
--red: #dc2626;
|
|
1433
|
+
--red-bg: #fee2e2;
|
|
1434
|
+
--yellow: #ca8a04;
|
|
1435
|
+
--yellow-bg: #fef9c3;
|
|
1436
|
+
--blue: #2563eb;
|
|
1437
|
+
--blue-bg: #dbeafe;
|
|
1438
|
+
--purple: #7c3aed;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
635
1441
|
* { 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:
|
|
1442
|
+
body { font-family: 'SF Mono', 'Fira Code', monospace; background: var(--bg-primary); color: var(--text-primary); overflow: hidden; transition: background 0.3s, color 0.3s; }
|
|
1443
|
+
.header { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
1444
|
+
.header h1 { font-size: 15px; color: var(--text-primary); }
|
|
1445
|
+
.header .sub { font-size: 11px; color: var(--text-muted); margin-left: 12px; }
|
|
1446
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
1447
|
+
.theme-toggle { background: var(--bg-surface); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: 6px; font-family: inherit; font-size: 11px; cursor: pointer; transition: all 0.2s; }
|
|
1448
|
+
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
|
640
1449
|
.status { font-size: 11px; padding: 3px 10px; border-radius: 10px; }
|
|
641
|
-
.status.idle { background:
|
|
642
|
-
.status.live { background:
|
|
643
|
-
.status.complete { background:
|
|
1450
|
+
.status.idle { background: var(--accent-bg); color: var(--accent); }
|
|
1451
|
+
.status.live { background: var(--green-bg); color: var(--green); animation: pulse 2s infinite; }
|
|
1452
|
+
.status.complete { background: var(--accent-bg); color: var(--accent); }
|
|
644
1453
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
645
1454
|
|
|
646
1455
|
.layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }
|
|
647
1456
|
|
|
648
1457
|
/* LEFT PANEL — Controls */
|
|
649
|
-
.controls { background:
|
|
1458
|
+
.controls { background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px; }
|
|
650
1459
|
.ctrl-section { margin-bottom: 20px; }
|
|
651
|
-
.ctrl-section h3 { font-size: 11px; color:
|
|
1460
|
+
.ctrl-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
|
|
652
1461
|
|
|
653
1462
|
.ctrl-row { margin-bottom: 12px; }
|
|
654
|
-
.ctrl-label { font-size: 11px; color:
|
|
655
|
-
.ctrl-label .val { color:
|
|
1463
|
+
.ctrl-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; display: flex; justify-content: space-between; }
|
|
1464
|
+
.ctrl-label .val { color: var(--text-primary); font-weight: 600; }
|
|
656
1465
|
|
|
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:
|
|
1466
|
+
input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: var(--bg-elevated); border-radius: 2px; outline: none; }
|
|
1467
|
+
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; }
|
|
1468
|
+
input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: none; }
|
|
660
1469
|
|
|
661
|
-
select { width: 100%; background:
|
|
1470
|
+
select { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
|
|
662
1471
|
|
|
663
1472
|
.toggle-row { display: flex; align-items: center; gap: 8px; }
|
|
664
|
-
.toggle { position: relative; width: 36px; height: 20px; background:
|
|
665
|
-
.toggle.on { background:
|
|
1473
|
+
.toggle { position: relative; width: 36px; height: 20px; background: var(--border-subtle); border-radius: 10px; cursor: pointer; transition: background 0.2s; }
|
|
1474
|
+
.toggle.on { background: var(--green); }
|
|
666
1475
|
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
|
|
667
1476
|
.toggle.on::after { transform: translateX(16px); }
|
|
668
|
-
.toggle-label { font-size: 12px; color:
|
|
1477
|
+
.toggle-label { font-size: 12px; color: var(--text-secondary); }
|
|
669
1478
|
|
|
670
1479
|
.inject-row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
671
1480
|
.inject-row select { flex: 1; }
|
|
672
|
-
.inject-row input { width: 50px; background:
|
|
1481
|
+
.inject-row input { width: 50px; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 4px 6px; border-radius: 4px; font-family: inherit; font-size: 12px; text-align: center; }
|
|
673
1482
|
.inject-list { margin-bottom: 8px; }
|
|
674
|
-
.inject-item { font-size: 11px; color:
|
|
675
|
-
.inject-item .remove { color:
|
|
1483
|
+
.inject-item { font-size: 11px; color: var(--text-secondary); padding: 3px 6px; background: var(--bg-surface); border-radius: 3px; margin-bottom: 3px; display: flex; justify-content: space-between; }
|
|
1484
|
+
.inject-item .remove { color: var(--red); cursor: pointer; }
|
|
676
1485
|
|
|
677
1486
|
.btn { width: 100%; padding: 10px; border: none; border-radius: 6px; font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s; }
|
|
678
|
-
.btn-run { background:
|
|
1487
|
+
.btn-run { background: var(--green); color: #0a0a0a; }
|
|
679
1488
|
.btn-run:hover { background: #22c55e; }
|
|
680
|
-
.btn-run:disabled { background:
|
|
681
|
-
.btn-add { background:
|
|
682
|
-
.btn-add:hover { background:
|
|
1489
|
+
.btn-run:disabled { background: var(--bg-elevated); color: var(--text-faint); cursor: not-allowed; }
|
|
1490
|
+
.btn-add { background: var(--bg-elevated); color: var(--accent); padding: 6px; font-size: 11px; }
|
|
1491
|
+
.btn-add:hover { background: var(--accent-bg); }
|
|
683
1492
|
|
|
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:
|
|
1493
|
+
.scenario-btn { display: block; width: 100%; padding: 8px 10px; margin-bottom: 4px; background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; color: var(--text-primary); font-family: inherit; font-size: 11px; text-align: left; cursor: pointer; }
|
|
1494
|
+
.scenario-btn:hover { border-color: var(--accent); background: var(--accent-bg); }
|
|
1495
|
+
.scenario-btn .stitle { font-weight: 600; color: var(--text-primary); }
|
|
1496
|
+
.scenario-btn .sdesc { color: var(--text-muted); font-size: 10px; margin-top: 2px; }
|
|
688
1497
|
|
|
689
1498
|
/* 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:
|
|
1499
|
+
.btn-save { background: var(--accent-bg); color: var(--accent); margin-top: 0; }
|
|
1500
|
+
.btn-save:hover { background: var(--accent-bg); filter: brightness(1.2); }
|
|
1501
|
+
.btn-confirm { flex: 1; background: var(--green); color: #0a0a0a; padding: 7px; font-size: 11px; }
|
|
1502
|
+
.btn-cancel { flex: 1; background: var(--bg-elevated); color: var(--text-muted); padding: 7px; font-size: 11px; }
|
|
1503
|
+
.save-input { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 7px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; }
|
|
1504
|
+
.save-input:focus { border-color: var(--accent); outline: none; }
|
|
696
1505
|
|
|
697
1506
|
/* 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:
|
|
1507
|
+
.variant-card { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; padding: 8px 10px; margin-bottom: 4px; cursor: pointer; position: relative; }
|
|
1508
|
+
.variant-card:hover { border-color: var(--green); }
|
|
1509
|
+
.variant-card .vname { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
1510
|
+
.variant-card .vdesc { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
|
1511
|
+
.variant-card .vmeta { font-size: 10px; color: var(--text-faint); margin-top: 4px; }
|
|
1512
|
+
.variant-card .vmeta .vresult { color: var(--green); }
|
|
1513
|
+
.variant-card .vdelete { position: absolute; top: 6px; right: 8px; color: var(--red); font-size: 10px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
|
|
705
1514
|
.variant-card:hover .vdelete { opacity: 1; }
|
|
706
|
-
.variant-card .vbase { display: inline-block; font-size: 9px; padding: 1px 5px; background:
|
|
1515
|
+
.variant-card .vbase { display: inline-block; font-size: 9px; padding: 1px 5px; background: var(--accent-bg); color: var(--accent); border-radius: 3px; margin-top: 3px; }
|
|
707
1516
|
|
|
708
1517
|
/* RIGHT PANEL — Simulation viewer */
|
|
709
1518
|
.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:
|
|
1519
|
+
.viewer-top { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); }
|
|
1520
|
+
.viewer-mid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); overflow: hidden; }
|
|
1521
|
+
.viewer-bottom { background: var(--bg-primary); border-top: 1px solid var(--border); padding: 12px 16px; max-height: 180px; overflow-y: auto; }
|
|
713
1522
|
|
|
714
|
-
.vpanel { background:
|
|
715
|
-
.vpanel h2 { font-size: 11px; color:
|
|
1523
|
+
.vpanel { background: var(--bg-primary); padding: 14px; overflow-y: auto; }
|
|
1524
|
+
.vpanel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
|
716
1525
|
|
|
717
1526
|
/* Metrics */
|
|
718
1527
|
.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:
|
|
1528
|
+
.metric-box { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 10px; text-align: center; }
|
|
1529
|
+
.metric-box .value { font-size: 20px; font-weight: 700; color: var(--text-primary); }
|
|
1530
|
+
.metric-box .label { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
|
1531
|
+
.metric-box.good .value { color: var(--green); }
|
|
1532
|
+
.metric-box.bad .value { color: var(--red); }
|
|
1533
|
+
.metric-box.warn .value { color: var(--yellow); }
|
|
725
1534
|
|
|
726
1535
|
/* Agent bars */
|
|
727
1536
|
.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:
|
|
1537
|
+
.agent-name { width: 130px; color: var(--text-secondary); flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1538
|
+
.impact-bar-bg { flex: 1; height: 14px; background: var(--bg-surface); border-radius: 3px; position: relative; overflow: hidden; }
|
|
730
1539
|
.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:
|
|
1540
|
+
.impact-bar.positive { background: var(--green); right: 50%; }
|
|
1541
|
+
.impact-bar.negative { background: var(--red); left: 50%; }
|
|
1542
|
+
.impact-val { width: 44px; text-align: right; color: var(--text-secondary); font-size: 10px; }
|
|
1543
|
+
.center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
|
|
735
1544
|
.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:
|
|
1545
|
+
.verdict.ALLOW { background: var(--green-bg); color: var(--green); }
|
|
1546
|
+
.verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1547
|
+
.verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
|
|
739
1548
|
|
|
740
1549
|
/* Chart */
|
|
741
1550
|
.chart-container { position: relative; height: 100%; min-height: 150px; }
|
|
742
1551
|
canvas { width: 100% !important; height: 100% !important; }
|
|
743
1552
|
|
|
744
1553
|
/* 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:
|
|
1554
|
+
.trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
1555
|
+
.trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
|
|
1556
|
+
.trace-round-header:hover { background: var(--bg-elevated); }
|
|
748
1557
|
.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:
|
|
1558
|
+
.trace-phase.baseline { background: var(--blue-bg); color: var(--blue); }
|
|
1559
|
+
.trace-phase.governed { background: var(--green-bg); color: var(--green); }
|
|
1560
|
+
.trace-round-label { font-size: 11px; color: var(--text-primary); font-weight: 600; }
|
|
1561
|
+
.trace-round-metrics { margin-left: auto; font-size: 10px; color: var(--text-muted); display: flex; gap: 10px; }
|
|
753
1562
|
.trace-body { padding: 0 10px 8px; }
|
|
754
1563
|
.trace-body[data-collapsed="true"] { display: none; }
|
|
755
1564
|
.trace-section { margin-top: 6px; }
|
|
756
|
-
.trace-section-label { font-size: 9px; color:
|
|
1565
|
+
.trace-section-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 3px; display: flex; align-items: center; gap: 5px; }
|
|
757
1566
|
.trace-section-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
|
|
758
1567
|
.trace-section-label.event::before { background: #f59e0b; }
|
|
759
1568
|
.trace-section-label.agents::before { background: #3b82f6; }
|
|
760
1569
|
.trace-section-label.governance::before { background: #10b981; }
|
|
761
|
-
.trace-event-item { font-size: 10px; color:
|
|
1570
|
+
.trace-event-item { font-size: 10px; color: var(--text-primary); padding: 3px 0 3px 11px; border-left: 2px solid #f59e0b; margin-left: 2px; }
|
|
762
1571
|
.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:
|
|
1572
|
+
.trace-event-severity.major, .trace-event-severity.extreme { background: var(--red-bg); color: var(--red); }
|
|
1573
|
+
.trace-event-severity.moderate { background: var(--yellow-bg); color: var(--yellow); }
|
|
1574
|
+
.trace-event-severity.minor { background: var(--green-bg); color: var(--green); }
|
|
766
1575
|
.trace-agent-item { font-size: 10px; padding: 2px 0 2px 11px; border-left: 2px solid #3b82f6; margin-left: 2px; display: flex; align-items: center; gap: 6px; }
|
|
767
|
-
.trace-agent-name { color:
|
|
768
|
-
.trace-agent-action { color:
|
|
1576
|
+
.trace-agent-name { color: var(--blue); font-weight: 500; min-width: 80px; }
|
|
1577
|
+
.trace-agent-action { color: var(--text-secondary); flex: 1; }
|
|
769
1578
|
.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:
|
|
1579
|
+
.trace-agent-impact.positive { color: var(--green); }
|
|
1580
|
+
.trace-agent-impact.negative { color: var(--red); }
|
|
772
1581
|
.trace-gov-item { font-size: 10px; padding: 3px 6px 3px 11px; border-left: 2px solid #10b981; margin-left: 2px; display: flex; align-items: center; gap: 6px; }
|
|
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:
|
|
1582
|
+
.trace-gov-rule { font-size: 9px; color: var(--text-muted); font-family: monospace; }
|
|
1583
|
+
.trace-gov-reason { color: var(--text-secondary); flex: 1; }
|
|
1584
|
+
.trace-dynamics { font-size: 10px; color: var(--purple); padding: 2px 0 2px 11px; border-left: 2px solid #7c3aed; margin-left: 2px; font-style: italic; }
|
|
1585
|
+
.trace-arrow { color: var(--text-faint); font-size: 10px; text-align: center; padding: 2px 0; }
|
|
1586
|
+
.trace-empty { font-size: 10px; color: var(--text-faint); font-style: italic; padding: 4px 0; }
|
|
778
1587
|
|
|
779
1588
|
/* World info */
|
|
780
|
-
.world-thesis { font-size: 11px; color:
|
|
781
|
-
.
|
|
1589
|
+
.world-thesis { font-size: 11px; color: var(--text-secondary); font-style: italic; margin-bottom: 8px; }
|
|
1590
|
+
.rule-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; transition: border-color 0.2s; }
|
|
1591
|
+
.rule-card:hover { border-color: var(--text-muted); }
|
|
1592
|
+
.rule-card.type-invariant { border-left: 4px solid var(--green); }
|
|
1593
|
+
.rule-card.type-gate { border-left: 4px solid var(--red); }
|
|
1594
|
+
.rule-card.type-warning { border-left: 4px solid var(--yellow); }
|
|
1595
|
+
.rule-card.type-modify { border-left: 4px solid #60a5fa; }
|
|
1596
|
+
.rule-card .rule-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
1597
|
+
.rule-card .rule-icon { font-size: 12px; flex-shrink: 0; }
|
|
1598
|
+
.rule-card .rule-title { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
1599
|
+
.rule-card .rule-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
|
|
1600
|
+
.rule-card .rule-meta { font-size: 10px; color: var(--text-muted); margin-top: 6px; opacity: 0.8; }
|
|
1601
|
+
.rule-card .rule-why { font-size: 10px; color: var(--text-muted); margin-top: 5px; font-style: italic; padding-top: 5px; border-top: 1px solid var(--border); }
|
|
1602
|
+
.rule-card .rule-impact { display: none; font-size: 10px; margin-top: 6px; padding: 5px 8px; border-radius: 4px; background: var(--bg-surface, var(--bg-secondary)); }
|
|
1603
|
+
.rule-card .rule-impact.visible { display: block; }
|
|
1604
|
+
.rule-card .rule-impact .impact-stat { color: var(--text-primary); font-weight: 600; }
|
|
1605
|
+
.rule-card .rule-impact .impact-label { color: var(--text-muted); }
|
|
1606
|
+
.rule-card.user-rule { border-left-color: #818cf8; background: rgba(129, 140, 248, 0.05); }
|
|
1607
|
+
.rule-source-tag { font-size: 9px; font-weight: 700; color: #818cf8; background: rgba(129, 140, 248, 0.15); padding: 1px 5px; border-radius: 3px; margin-left: auto; letter-spacing: 0.5px; }
|
|
782
1608
|
|
|
783
1609
|
/* Empty state */
|
|
784
|
-
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color:
|
|
1610
|
+
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-faint); }
|
|
785
1611
|
.empty-state .icon { font-size: 48px; margin-bottom: 12px; }
|
|
786
|
-
.empty-state .msg { font-size: 13px; }
|
|
787
|
-
.empty-state .hint { font-size: 11px; color:
|
|
1612
|
+
.empty-state .msg { font-size: 13px; color: var(--text-muted); }
|
|
1613
|
+
.empty-state .hint { font-size: 11px; color: var(--text-faint); margin-top: 6px; }
|
|
788
1614
|
|
|
789
1615
|
/* System Shift Card */
|
|
790
|
-
.system-shift { display: none; margin: 12px 16px; border: 1px solid
|
|
1616
|
+
.system-shift { display: none; margin: 12px 16px; border: 1px solid var(--green-bg); border-radius: 8px; background: var(--bg-secondary); overflow: hidden; animation: fadeInUp 0.4s ease; }
|
|
791
1617
|
.system-shift.visible { display: block; }
|
|
792
1618
|
@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:
|
|
1619
|
+
.ss-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background: var(--green-bg); border-bottom: 1px solid var(--green-bg); }
|
|
1620
|
+
.ss-icon { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgba(74,222,128,0.4); }
|
|
1621
|
+
.ss-title { font-size: 11px; font-weight: 700; color: var(--green); text-transform: uppercase; letter-spacing: 1.5px; }
|
|
1622
|
+
.ss-rule { font-size: 13px; font-weight: 600; color: var(--text-primary); padding: 10px 14px 0; }
|
|
797
1623
|
.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:
|
|
1624
|
+
.ss-section { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
|
|
1625
|
+
.ss-section-label { font-size: 9px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
|
|
1626
|
+
.ss-adapt-rate { font-size: 20px; font-weight: 700; color: var(--green); }
|
|
1627
|
+
.ss-adapt-desc { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
|
|
1628
|
+
.ss-shift-item { font-size: 11px; color: var(--text-secondary); padding: 2px 0; display: flex; align-items: center; gap: 6px; }
|
|
1629
|
+
.ss-shift-arrow { color: var(--green); font-weight: 600; }
|
|
1630
|
+
.ss-pattern-tag { display: inline-block; font-size: 10px; padding: 2px 8px; background: var(--accent-bg); color: var(--accent); border-radius: 3px; margin-right: 4px; margin-bottom: 4px; }
|
|
1631
|
+
.ss-impact-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--text-secondary); padding: 2px 0; }
|
|
1632
|
+
.ss-impact-delta { color: var(--green); font-weight: 600; }
|
|
1633
|
+
.ss-impact-delta.negative { color: var(--red); }
|
|
1634
|
+
.ss-narrative { font-size: 12px; color: var(--text-primary); line-height: 1.5; font-style: italic; border-left: 2px solid var(--green); padding-left: 10px; }
|
|
1635
|
+
.ss-scale { font-size: 10px; color: var(--text-muted); padding: 0 14px 6px; }
|
|
1636
|
+
.ss-scale strong { color: var(--text-secondary); }
|
|
1637
|
+
.ss-flow { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 10px; color: var(--text-faint); }
|
|
1638
|
+
.ss-flow-arrow { color: var(--green); }
|
|
1639
|
+
.ss-raw-toggle { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: var(--bg-secondary); border: none; border-top: 1px solid var(--border); color: var(--text-muted); font-family: inherit; font-size: 10px; cursor: pointer; width: 100%; text-align: left; transition: color 0.2s; }
|
|
1640
|
+
.ss-raw-toggle:hover { color: var(--text-secondary); }
|
|
815
1641
|
.ss-raw-toggle .arrow { transition: transform 0.2s; }
|
|
816
1642
|
.ss-raw-toggle.open .arrow { transform: rotate(90deg); }
|
|
817
1643
|
.ss-raw-detail { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
|
|
818
1644
|
.ss-raw-detail.open { max-height: 200px; overflow-y: auto; }
|
|
819
1645
|
.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:
|
|
1646
|
+
.ss-raw-item { font-size: 10px; color: var(--text-muted); padding: 2px 0; display: flex; gap: 6px; }
|
|
1647
|
+
.ss-raw-item .raw-agent { color: var(--text-secondary); min-width: 100px; }
|
|
1648
|
+
.ss-raw-item .raw-action { color: var(--text-secondary); flex: 1; }
|
|
823
1649
|
.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:
|
|
1650
|
+
.ss-raw-item .raw-verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1651
|
+
.ss-raw-item .raw-verdict.MODIFY { background: var(--yellow-bg); color: var(--yellow); }
|
|
826
1652
|
|
|
827
1653
|
/* 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:
|
|
1654
|
+
.integrate-section { background: var(--accent-bg); border: 1px solid var(--accent-bg); border-radius: 6px; padding: 10px 12px; margin-top: 8px; }
|
|
1655
|
+
.integrate-section h4 { font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
|
1656
|
+
.integrate-code { font-size: 10px; color: var(--text-primary); background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 4px; padding: 8px; overflow-x: auto; white-space: pre; line-height: 1.5; }
|
|
1657
|
+
.integrate-code .kw { color: var(--accent); }
|
|
1658
|
+
.integrate-code .str { color: var(--green); }
|
|
1659
|
+
.integrate-code .comment { color: var(--text-muted); }
|
|
1660
|
+
.integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
|
|
1661
|
+
.integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
|
|
1662
|
+
|
|
1663
|
+
/* Rule editor */
|
|
1664
|
+
.rule-editor { margin-top: 8px; }
|
|
1665
|
+
.rule-input { width: 100%; min-height: 60px; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 12px; resize: vertical; line-height: 1.5; }
|
|
1666
|
+
.rule-input:focus { border-color: var(--accent); outline: none; }
|
|
1667
|
+
.rule-input::placeholder { color: var(--text-faint); }
|
|
1668
|
+
.btn-parse { background: var(--accent-bg); color: var(--accent); margin-top: 6px; padding: 8px; font-size: 11px; }
|
|
1669
|
+
.btn-parse:hover { filter: brightness(1.2); }
|
|
1670
|
+
.btn-parse:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1671
|
+
.parsed-rules { margin-top: 8px; }
|
|
1672
|
+
.parsed-rule { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; transition: border-color 0.2s; }
|
|
1673
|
+
.parsed-rule:hover { border-color: var(--text-muted); }
|
|
1674
|
+
.parsed-rule.enforcement-block { border-left: 4px solid var(--red); }
|
|
1675
|
+
.parsed-rule.enforcement-allow { border-left: 4px solid var(--green); }
|
|
1676
|
+
.parsed-rule.enforcement-modify { border-left: 4px solid #60a5fa; }
|
|
1677
|
+
.parsed-rule.enforcement-warn { border-left: 4px solid var(--yellow); }
|
|
1678
|
+
.parsed-rule .pr-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
1679
|
+
.parsed-rule .pr-icon { font-size: 12px; }
|
|
1680
|
+
.parsed-rule .pr-action { font-weight: 700; font-size: 12px; color: var(--text-primary); text-transform: capitalize; }
|
|
1681
|
+
.parsed-rule .pr-desc { color: var(--text-secondary); font-size: 11px; line-height: 1.4; }
|
|
1682
|
+
.parsed-rule .pr-patterns { color: var(--text-muted); font-size: 10px; margin-top: 6px; opacity: 0.8; }
|
|
1683
|
+
.btn-apply-rules { background: var(--green); color: #0a0a0a; margin-top: 6px; padding: 8px; font-size: 11px; }
|
|
1684
|
+
.btn-apply-rules:hover { filter: brightness(0.9); }
|
|
1685
|
+
.rule-status { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
|
|
1686
|
+
.rule-status.success { color: var(--green); }
|
|
1687
|
+
.rule-status.error { color: var(--red); }
|
|
1688
|
+
.rule-examples { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.6; }
|
|
1689
|
+
.rule-examples code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
|
|
836
1690
|
</style>
|
|
837
1691
|
</head>
|
|
838
1692
|
<body>
|
|
@@ -841,7 +1695,10 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
841
1695
|
<h1>NV-SIM</h1>
|
|
842
1696
|
<span class="sub">Scenario Control Platform</span>
|
|
843
1697
|
</div>
|
|
844
|
-
<
|
|
1698
|
+
<div class="header-right">
|
|
1699
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">Light Mode</button>
|
|
1700
|
+
<span id="status" class="status idle">Ready</span>
|
|
1701
|
+
</div>
|
|
845
1702
|
</div>
|
|
846
1703
|
|
|
847
1704
|
<div class="layout">
|
|
@@ -873,6 +1730,25 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
873
1730
|
<div id="state-vars"></div>
|
|
874
1731
|
</div>
|
|
875
1732
|
|
|
1733
|
+
<!-- Plain-English Rule Editor -->
|
|
1734
|
+
<div class="ctrl-section">
|
|
1735
|
+
<h3>Write Rules in Plain English</h3>
|
|
1736
|
+
<div class="rule-editor">
|
|
1737
|
+
<textarea class="rule-input" id="rule-input" placeholder="Type rules in plain English, one per line: Block panic selling during high volatility Limit leverage to 5x maximum Pause any trade over $10M for review"></textarea>
|
|
1738
|
+
<button class="btn btn-parse" id="parse-rules-btn">Parse Rules</button>
|
|
1739
|
+
<div id="parsed-rules" class="parsed-rules"></div>
|
|
1740
|
+
<div id="rule-status" class="rule-status"></div>
|
|
1741
|
+
<div class="rule-examples">
|
|
1742
|
+
Examples:<br>
|
|
1743
|
+
<code>Block panic selling</code><br>
|
|
1744
|
+
<code>Limit leverage to 3x</code><br>
|
|
1745
|
+
<code>Pause large trades for review</code><br>
|
|
1746
|
+
<code>Allow hedging positions</code><br>
|
|
1747
|
+
<code>Block short selling during circuit breaker</code>
|
|
1748
|
+
</div>
|
|
1749
|
+
</div>
|
|
1750
|
+
</div>
|
|
1751
|
+
|
|
876
1752
|
<!-- Scenario presets -->
|
|
877
1753
|
<div class="ctrl-section">
|
|
878
1754
|
<h3>Scenario Presets</h3>
|
|
@@ -951,6 +1827,24 @@ verdict = evaluate(
|
|
|
951
1827
|
<span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
|
|
952
1828
|
</div>
|
|
953
1829
|
</div>
|
|
1830
|
+
|
|
1831
|
+
<!-- Session Report Panel -->
|
|
1832
|
+
<div class="ctrl-section" id="session-panel">
|
|
1833
|
+
<h3 class="ctrl-label">SESSION</h3>
|
|
1834
|
+
<div class="metric-grid" style="grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
|
|
1835
|
+
<div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
|
|
1836
|
+
<div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
|
|
1837
|
+
<div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
|
|
1838
|
+
<div class="metric-box"><div class="value" id="s-allowed" style="color:#4ade80">0</div><div class="label">Allowed</div></div>
|
|
1839
|
+
</div>
|
|
1840
|
+
<div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
|
|
1841
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
1842
|
+
<button onclick="viewSessionReport()" style="flex:1;padding:6px 8px;background:#1e1b4b;color:#818cf8;border:1px solid #312e81;border-radius:4px;cursor:pointer;font-size:11px">View Report</button>
|
|
1843
|
+
<button onclick="resetSession()" style="flex:1;padding:6px 8px;background:#1c1917;color:#a8a29e;border:1px solid #292524;border-radius:4px;cursor:pointer;font-size:11px">Reset & Compare</button>
|
|
1844
|
+
<button onclick="saveExperiment()" style="flex:1;padding:6px 8px;background:#052e16;color:#4ade80;border:1px solid #14532d;border-radius:4px;cursor:pointer;font-size:11px">Save</button>
|
|
1845
|
+
</div>
|
|
1846
|
+
<div id="session-history" style="margin-top:6px;font-size:10px;color:#666"></div>
|
|
1847
|
+
</div>
|
|
954
1848
|
</div>
|
|
955
1849
|
</div>
|
|
956
1850
|
|
|
@@ -1050,6 +1944,7 @@ let governedImpacts = [];
|
|
|
1050
1944
|
let chartLabels = [];
|
|
1051
1945
|
let chart = null;
|
|
1052
1946
|
let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
|
|
1947
|
+
let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
|
|
1053
1948
|
|
|
1054
1949
|
const statusEl = document.getElementById('status');
|
|
1055
1950
|
const worldSelect = document.getElementById('world-select');
|
|
@@ -1321,10 +2216,10 @@ function initChart() {
|
|
|
1321
2216
|
animation: { duration: 400 },
|
|
1322
2217
|
responsive: true,
|
|
1323
2218
|
maintainAspectRatio: false,
|
|
1324
|
-
plugins: { legend: { labels: { color: '#888', font: { family: 'monospace', size: 10 } } } },
|
|
2219
|
+
plugins: { legend: { labels: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888', font: { family: 'monospace', size: 10 } } } },
|
|
1325
2220
|
scales: {
|
|
1326
|
-
x: { ticks: { color: '#
|
|
1327
|
-
y: { ticks: { color: '#
|
|
2221
|
+
x: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' } },
|
|
2222
|
+
y: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' }, min: -1, max: 1 }
|
|
1328
2223
|
}
|
|
1329
2224
|
}
|
|
1330
2225
|
});
|
|
@@ -1461,11 +2356,65 @@ function handleEvent(event) {
|
|
|
1461
2356
|
if (!narrativeEventsByRound[ev.round]) narrativeEventsByRound[ev.round] = [];
|
|
1462
2357
|
narrativeEventsByRound[ev.round].push(ev);
|
|
1463
2358
|
});
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
2359
|
+
function parseRuleTitle(id, text) {
|
|
2360
|
+
var parts = text.split(/\s*[—–-]{1,}\s*/);
|
|
2361
|
+
var title = parts.length > 1 ? parts[0].trim() : id.replace(/[-_]/g, ' ');
|
|
2362
|
+
var desc = parts.length > 1 ? parts.slice(1).join(' — ').trim() : text;
|
|
2363
|
+
title = title.replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
|
2364
|
+
return { title: title, desc: desc };
|
|
2365
|
+
}
|
|
2366
|
+
function generateWhy(text, type) {
|
|
2367
|
+
var t = text.toLowerCase();
|
|
2368
|
+
if (t.includes('liquidity') || t.includes('drain')) return 'Prevents system collapse from liquidity crises';
|
|
2369
|
+
if (t.includes('panic') || t.includes('cascade')) return 'Stops runaway feedback loops during market shocks';
|
|
2370
|
+
if (t.includes('leverage')) return 'Limits systemic risk from over-leveraged positions';
|
|
2371
|
+
if (t.includes('price') || t.includes('pricing')) return 'Stabilizes pricing mechanisms during volatility';
|
|
2372
|
+
if (t.includes('sentiment') || t.includes('consumer')) return 'Tracks behavioral feedback between price and confidence';
|
|
2373
|
+
if (t.includes('military') || t.includes('escalat')) return 'Models how escalation constrains available options';
|
|
2374
|
+
if (t.includes('diplomatic') || t.includes('window')) return 'Preserves negotiation pathways before they close';
|
|
2375
|
+
if (t.includes('grid') || t.includes('capacity')) return 'Prevents infrastructure overload from demand surges';
|
|
2376
|
+
if (t.includes('election') || t.includes('political')) return 'Captures how political pressure amplifies responses';
|
|
2377
|
+
if (t.includes('supply') || t.includes('energy') || t.includes('oil')) return 'Guards against cascading supply chain disruptions';
|
|
2378
|
+
if (t.includes('fraud') || t.includes('suspicious')) return 'Detects and contains anomalous behavior patterns';
|
|
2379
|
+
if (t.includes('withdraw') || t.includes('limit')) return 'Constrains individual actions to protect system stability';
|
|
2380
|
+
if (type === 'gate') return 'Blocks dangerous actions before they propagate';
|
|
2381
|
+
if (type === 'warning') return 'Provides early warning before thresholds are breached';
|
|
2382
|
+
return 'Maintains system integrity under stress conditions';
|
|
2383
|
+
}
|
|
2384
|
+
// Reset per-rule impact counters
|
|
2385
|
+
ruleImpactTracker = {};
|
|
2386
|
+
activeInvEl.innerHTML = event.invariants.map(function(inv) {
|
|
2387
|
+
var parsed = parseRuleTitle(inv.id, inv.description);
|
|
2388
|
+
var why = generateWhy(inv.description, 'invariant');
|
|
2389
|
+
var isUser = inv.source === 'user';
|
|
2390
|
+
var isFull = inv.enforcement === 'full';
|
|
2391
|
+
var enfLabel = isFull ? 'Fully enforced across system' : 'Advisory only';
|
|
2392
|
+
var enfIcon = isFull ? '✓' : '⚠';
|
|
2393
|
+
var sourceTag = isUser ? '<span class="rule-source-tag">USER RULE</span>' : '';
|
|
2394
|
+
ruleImpactTracker[inv.id] = { blocks: 0, label: parsed.title };
|
|
2395
|
+
return '<div class="rule-card type-invariant' + (isUser ? ' user-rule' : '') + '" data-rule-id="' + inv.id + '">' +
|
|
2396
|
+
'<div class="rule-header"><span class="rule-icon">🟢</span><span class="rule-title">' + parsed.title + '</span>' + sourceTag + '</div>' +
|
|
2397
|
+
'<div class="rule-desc">' + parsed.desc + '</div>' +
|
|
2398
|
+
'<div class="rule-meta">Invariant • ' + enfIcon + ' ' + enfLabel + '</div>' +
|
|
2399
|
+
'<div class="rule-why">' + why + '</div>' +
|
|
2400
|
+
'<div class="rule-impact" data-impact-id="' + inv.id + '"></div>' +
|
|
2401
|
+
'</div>';
|
|
2402
|
+
}).join('') + event.gates.map(function(g) {
|
|
2403
|
+
var isCritical = g.severity === 'critical';
|
|
2404
|
+
var typeClass = isCritical ? 'type-gate' : 'type-warning';
|
|
2405
|
+
var icon = isCritical ? '🔴' : '🟡';
|
|
2406
|
+
var typeLabel = isCritical ? 'Gate' : 'Warning';
|
|
2407
|
+
var effect = isCritical ? 'Blocks actions' : 'Signals risk';
|
|
2408
|
+
var why = generateWhy(g.label + ' ' + (g.condition || ''), isCritical ? 'gate' : 'warning');
|
|
2409
|
+
ruleImpactTracker[g.id] = { blocks: 0, label: g.label };
|
|
2410
|
+
return '<div class="rule-card ' + typeClass + '" data-rule-id="' + g.id + '">' +
|
|
2411
|
+
'<div class="rule-header"><span class="rule-icon">' + icon + '</span><span class="rule-title">' + g.label + '</span></div>' +
|
|
2412
|
+
'<div class="rule-desc">' + (g.condition || g.label) + '</div>' +
|
|
2413
|
+
'<div class="rule-meta">' + typeLabel + ' • ' + effect + '</div>' +
|
|
2414
|
+
'<div class="rule-why">' + why + '</div>' +
|
|
2415
|
+
'<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
|
|
2416
|
+
'</div>';
|
|
2417
|
+
}).join('');
|
|
1469
2418
|
initChart();
|
|
1470
2419
|
}
|
|
1471
2420
|
|
|
@@ -1502,6 +2451,8 @@ function handleEvent(event) {
|
|
|
1502
2451
|
document.getElementById('m-stability').parentElement.className = 'metric-box ' + (r.governed.metrics.stabilityScore > 0.7 ? 'good' : 'warn');
|
|
1503
2452
|
addLog('Complete. Governance effectiveness: ' + (r.comparison.governanceEffectiveness * 100).toFixed(0) + '%');
|
|
1504
2453
|
renderSystemShift(r);
|
|
2454
|
+
renderRuleImpacts(r);
|
|
2455
|
+
renderEnforcementClassification(r.enforcementClassification || []);
|
|
1505
2456
|
lastSimResult = {
|
|
1506
2457
|
stability: r.governed.metrics.stabilityScore,
|
|
1507
2458
|
volatility: r.governed.metrics.maxVolatility,
|
|
@@ -1514,6 +2465,59 @@ function handleEvent(event) {
|
|
|
1514
2465
|
}
|
|
1515
2466
|
}
|
|
1516
2467
|
|
|
2468
|
+
// ============================================
|
|
2469
|
+
// PER-RULE IMPACT RENDERING
|
|
2470
|
+
// ============================================
|
|
2471
|
+
function renderRuleImpacts(result) {
|
|
2472
|
+
var totalBlocks = shiftTracker.blocks;
|
|
2473
|
+
var cascadeAvoided = result && result.governed && result.baseline &&
|
|
2474
|
+
result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
|
|
2475
|
+
|
|
2476
|
+
Object.keys(ruleImpactTracker).forEach(function(ruleId) {
|
|
2477
|
+
var tracker = ruleImpactTracker[ruleId];
|
|
2478
|
+
var el = document.querySelector('[data-impact-id="' + ruleId + '"]');
|
|
2479
|
+
if (!el) return;
|
|
2480
|
+
|
|
2481
|
+
var html = '';
|
|
2482
|
+
if (tracker.blocks > 0) {
|
|
2483
|
+
html += '<span class="impact-stat">' + tracker.blocks + ' action' + (tracker.blocks > 1 ? 's' : '') + ' blocked</span>';
|
|
2484
|
+
if (cascadeAvoided) {
|
|
2485
|
+
html += ' <span class="impact-label">→ cascade avoided</span>';
|
|
2486
|
+
} else {
|
|
2487
|
+
html += ' <span class="impact-label">→ behavior modified</span>';
|
|
2488
|
+
}
|
|
2489
|
+
} else {
|
|
2490
|
+
// Rule was present but didn't fire — still useful info
|
|
2491
|
+
html += '<span class="impact-label">No violations detected — agents complied</span>';
|
|
2492
|
+
}
|
|
2493
|
+
el.innerHTML = html;
|
|
2494
|
+
el.classList.add('visible');
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// ============================================
|
|
2499
|
+
// ENFORCEMENT CLASSIFICATION — Full vs Advisory
|
|
2500
|
+
// ============================================
|
|
2501
|
+
function renderEnforcementClassification(entries) {
|
|
2502
|
+
if (!entries || entries.length === 0) return;
|
|
2503
|
+
entries.forEach(function(entry) {
|
|
2504
|
+
var card = document.querySelector('[data-rule-id="' + entry.id + '"]');
|
|
2505
|
+
if (!card) return;
|
|
2506
|
+
var metaEl = card.querySelector('.rule-meta');
|
|
2507
|
+
if (!metaEl) return;
|
|
2508
|
+
if (entry.level === 'full' && entry.fired) {
|
|
2509
|
+
metaEl.innerHTML = 'Invariant • ✓ Fully enforced • <span style="color:#22c55e;font-weight:600">FIRED</span>';
|
|
2510
|
+
card.style.borderLeftColor = '#22c55e';
|
|
2511
|
+
} else if (entry.level === 'full' && !entry.fired) {
|
|
2512
|
+
metaEl.innerHTML = 'Invariant • ✓ Fully enforced • <span style="color:var(--text-muted)">standby</span>';
|
|
2513
|
+
} else if (entry.level === 'advisory') {
|
|
2514
|
+
metaEl.innerHTML = 'Invariant • ⚠ Advisory only • <span style="color:var(--text-muted)">monitored</span>';
|
|
2515
|
+
card.style.borderLeftColor = '#6b7280';
|
|
2516
|
+
card.style.opacity = '0.75';
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
|
|
1517
2521
|
// ============================================
|
|
1518
2522
|
// SYSTEM SHIFT CARD — The Demo Moment
|
|
1519
2523
|
// ============================================
|
|
@@ -1551,6 +2555,18 @@ function trackShift(event) {
|
|
|
1551
2555
|
governed.forEach(function(r) {
|
|
1552
2556
|
const key = r.verdict.status + ': ' + (r.reaction || 'adapted');
|
|
1553
2557
|
shiftTracker.shifts[key] = (shiftTracker.shifts[key] || 0) + 1;
|
|
2558
|
+
// Track per-rule impacts for card display
|
|
2559
|
+
var ruleId = r.verdict.ruleId || '';
|
|
2560
|
+
if (ruleId && ruleImpactTracker[ruleId]) {
|
|
2561
|
+
ruleImpactTracker[ruleId].blocks++;
|
|
2562
|
+
} else {
|
|
2563
|
+
// Try to match by pattern — governance may report with NV- prefix
|
|
2564
|
+
Object.keys(ruleImpactTracker).forEach(function(k) {
|
|
2565
|
+
if (ruleId.includes(k) || (r.verdict.reason && r.verdict.reason.toLowerCase().includes(ruleImpactTracker[k].label.toLowerCase()))) {
|
|
2566
|
+
ruleImpactTracker[k].blocks++;
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
1554
2570
|
// Store raw governed reactions for detail view
|
|
1555
2571
|
shiftTracker.rawGoverned.push({
|
|
1556
2572
|
agent: r.stakeholder_id,
|
|
@@ -1848,6 +2864,196 @@ handleEvent = function(event) {
|
|
|
1848
2864
|
}
|
|
1849
2865
|
};
|
|
1850
2866
|
|
|
2867
|
+
// ============================================
|
|
2868
|
+
// THEME TOGGLE — Light / Dark mode
|
|
2869
|
+
// ============================================
|
|
2870
|
+
const themeToggleBtn = document.getElementById('theme-toggle');
|
|
2871
|
+
function applyTheme(theme) {
|
|
2872
|
+
if (theme === 'light') {
|
|
2873
|
+
document.body.classList.add('light');
|
|
2874
|
+
themeToggleBtn.textContent = 'Dark Mode';
|
|
2875
|
+
} else {
|
|
2876
|
+
document.body.classList.remove('light');
|
|
2877
|
+
themeToggleBtn.textContent = 'Light Mode';
|
|
2878
|
+
}
|
|
2879
|
+
localStorage.setItem('nv-theme', theme);
|
|
2880
|
+
// Update chart colors if chart exists
|
|
2881
|
+
if (chart && chart.options) {
|
|
2882
|
+
const gridColor = theme === 'light' ? '#d4d4d4' : '#2a2a2a';
|
|
2883
|
+
const tickColor = theme === 'light' ? '#666' : '#888';
|
|
2884
|
+
const legendColor = theme === 'light' ? '#444' : '#888';
|
|
2885
|
+
chart.options.scales.x.ticks.color = tickColor;
|
|
2886
|
+
chart.options.scales.x.grid.color = gridColor;
|
|
2887
|
+
chart.options.scales.y.ticks.color = tickColor;
|
|
2888
|
+
chart.options.scales.y.grid.color = gridColor;
|
|
2889
|
+
chart.options.plugins.legend.labels.color = legendColor;
|
|
2890
|
+
chart.update();
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
themeToggleBtn.addEventListener('click', () => {
|
|
2894
|
+
const current = document.body.classList.contains('light') ? 'light' : 'dark';
|
|
2895
|
+
applyTheme(current === 'light' ? 'dark' : 'light');
|
|
2896
|
+
});
|
|
2897
|
+
// Restore saved theme
|
|
2898
|
+
const savedTheme = localStorage.getItem('nv-theme');
|
|
2899
|
+
if (savedTheme) applyTheme(savedTheme);
|
|
2900
|
+
|
|
2901
|
+
// ============================================
|
|
2902
|
+
// PLAIN-ENGLISH RULE EDITOR
|
|
2903
|
+
// ============================================
|
|
2904
|
+
const ruleInput = document.getElementById('rule-input');
|
|
2905
|
+
const parseRulesBtn = document.getElementById('parse-rules-btn');
|
|
2906
|
+
const parsedRulesEl = document.getElementById('parsed-rules');
|
|
2907
|
+
const ruleStatusEl = document.getElementById('rule-status');
|
|
2908
|
+
let parsedRuleData = [];
|
|
2909
|
+
|
|
2910
|
+
parseRulesBtn.addEventListener('click', async () => {
|
|
2911
|
+
const text = ruleInput.value.trim();
|
|
2912
|
+
if (!text) return;
|
|
2913
|
+
|
|
2914
|
+
parseRulesBtn.disabled = true;
|
|
2915
|
+
parseRulesBtn.textContent = 'Parsing...';
|
|
2916
|
+
ruleStatusEl.textContent = '';
|
|
2917
|
+
ruleStatusEl.className = 'rule-status';
|
|
2918
|
+
|
|
2919
|
+
try {
|
|
2920
|
+
const resp = await fetch('/api/parse-rules', {
|
|
2921
|
+
method: 'POST',
|
|
2922
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2923
|
+
body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
|
|
2924
|
+
});
|
|
2925
|
+
const data = await resp.json();
|
|
2926
|
+
|
|
2927
|
+
if (data.error) {
|
|
2928
|
+
ruleStatusEl.textContent = data.error;
|
|
2929
|
+
ruleStatusEl.className = 'rule-status error';
|
|
2930
|
+
parsedRulesEl.innerHTML = '';
|
|
2931
|
+
parsedRuleData = [];
|
|
2932
|
+
} else {
|
|
2933
|
+
parsedRuleData = data.rules || [];
|
|
2934
|
+
parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
|
|
2935
|
+
const enfType = r.enforcement || 'block';
|
|
2936
|
+
const iconMap = { block: '🔴', allow: '🟢', modify: '🔵', warn: '🟡', pause: '🟡' };
|
|
2937
|
+
const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
|
|
2938
|
+
const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
|
|
2939
|
+
const icon = iconMap[enfType] || '🟢';
|
|
2940
|
+
const label = labelMap[enfType] || 'Rule';
|
|
2941
|
+
const effect = effectMap[enfType] || 'Active';
|
|
2942
|
+
return '<div class="parsed-rule enforcement-' + enfType + '">' +
|
|
2943
|
+
'<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
|
|
2944
|
+
'<div class="pr-desc">' + r.description + '</div>' +
|
|
2945
|
+
'<div class="pr-patterns">' + effect + ' • Matches: ' + r.intent_patterns.join(', ') + '</div>' +
|
|
2946
|
+
'</div>';
|
|
2947
|
+
}).join('');
|
|
2948
|
+
|
|
2949
|
+
if (parsedRuleData.length > 0) {
|
|
2950
|
+
parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation</button>';
|
|
2951
|
+
document.getElementById('apply-rules-btn').addEventListener('click', async () => {
|
|
2952
|
+
try {
|
|
2953
|
+
const applyResp = await fetch('/api/apply-rules', {
|
|
2954
|
+
method: 'POST',
|
|
2955
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2956
|
+
body: JSON.stringify({ rules: parsedRuleData, worldId: currentWorld ? currentWorld.id : 'trading' }),
|
|
2957
|
+
});
|
|
2958
|
+
const applyData = await applyResp.json();
|
|
2959
|
+
if (applyData.status === 'applied') {
|
|
2960
|
+
ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
|
|
2961
|
+
ruleStatusEl.className = 'rule-status success';
|
|
2962
|
+
}
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
|
|
2965
|
+
ruleStatusEl.className = 'rule-status error';
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2968
|
+
ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click Apply.';
|
|
2969
|
+
ruleStatusEl.className = 'rule-status success';
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
ruleStatusEl.textContent = 'Error: ' + err.message;
|
|
2974
|
+
ruleStatusEl.className = 'rule-status error';
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
parseRulesBtn.disabled = false;
|
|
2978
|
+
parseRulesBtn.textContent = 'Parse Rules';
|
|
2979
|
+
});
|
|
2980
|
+
|
|
2981
|
+
// ============================================
|
|
2982
|
+
// SESSION TRACKING
|
|
2983
|
+
// ============================================
|
|
2984
|
+
|
|
2985
|
+
let sessionPollInterval = null;
|
|
2986
|
+
|
|
2987
|
+
async function pollSessionStats() {
|
|
2988
|
+
try {
|
|
2989
|
+
const resp = await fetch('/api/session');
|
|
2990
|
+
const data = await resp.json();
|
|
2991
|
+
const el = (id) => document.getElementById(id);
|
|
2992
|
+
if (el('s-total')) el('s-total').textContent = data.evaluations.total;
|
|
2993
|
+
if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
|
|
2994
|
+
if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
|
|
2995
|
+
if (el('s-allowed')) el('s-allowed').textContent = data.evaluations.allowed;
|
|
2996
|
+
if (el('s-agents')) {
|
|
2997
|
+
el('s-agents').textContent = data.agents.length > 0
|
|
2998
|
+
? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
|
|
2999
|
+
: 'No agents connected yet';
|
|
3000
|
+
}
|
|
3001
|
+
if (el('session-history') && data.historyCount > 0) {
|
|
3002
|
+
el('session-history').textContent = data.historyCount + ' previous session(s) saved for comparison';
|
|
3003
|
+
}
|
|
3004
|
+
} catch {}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
async function viewSessionReport() {
|
|
3008
|
+
try {
|
|
3009
|
+
const resp = await fetch('/api/session/report');
|
|
3010
|
+
const text = await resp.text();
|
|
3011
|
+
const log = document.getElementById('sim-log');
|
|
3012
|
+
if (log) {
|
|
3013
|
+
const div = document.createElement('div');
|
|
3014
|
+
div.className = 'log-round';
|
|
3015
|
+
div.innerHTML = '<h4 style="color:#818cf8">Enforcement Report</h4><pre style="white-space:pre-wrap;font-size:11px;color:#d4d4d8">' + text.replace(/</g,'<') + '</pre>';
|
|
3016
|
+
log.prepend(div);
|
|
3017
|
+
log.scrollTop = 0;
|
|
3018
|
+
}
|
|
3019
|
+
} catch (err) { console.error('Failed to load report', err); }
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
async function resetSession() {
|
|
3023
|
+
if (!confirm('Reset session? Current data will be saved for comparison.')) return;
|
|
3024
|
+
try {
|
|
3025
|
+
const resp = await fetch('/api/session/reset', { method: 'POST' });
|
|
3026
|
+
const data = await resp.json();
|
|
3027
|
+
const log = document.getElementById('sim-log');
|
|
3028
|
+
if (log) {
|
|
3029
|
+
const div = document.createElement('div');
|
|
3030
|
+
div.className = 'log-round';
|
|
3031
|
+
div.innerHTML = '<h4 style="color:#fbbf24">Session Reset</h4><div style="font-size:11px;color:#a8a29e">' + data.message + '</div>';
|
|
3032
|
+
log.prepend(div);
|
|
3033
|
+
}
|
|
3034
|
+
pollSessionStats();
|
|
3035
|
+
} catch (err) { console.error('Failed to reset session', err); }
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
async function saveExperiment() {
|
|
3039
|
+
try {
|
|
3040
|
+
const resp = await fetch('/api/session/save', { method: 'POST' });
|
|
3041
|
+
const data = await resp.json();
|
|
3042
|
+
if (data.error) { alert(data.error); return; }
|
|
3043
|
+
const log = document.getElementById('sim-log');
|
|
3044
|
+
if (log) {
|
|
3045
|
+
const div = document.createElement('div');
|
|
3046
|
+
div.className = 'log-round';
|
|
3047
|
+
div.innerHTML = '<h4 style="color:#4ade80">Experiment Saved</h4><div style="font-size:11px;color:#a8a29e">ID: ' + data.experimentId + '<br>File: ' + data.filePath + '<br>Stability: ' + (data.metrics.stability * 100).toFixed(0) + '% | Effectiveness: ' + (data.metrics.effectiveness * 100).toFixed(0) + '%</div>';
|
|
3048
|
+
log.prepend(div);
|
|
3049
|
+
}
|
|
3050
|
+
} catch (err) { console.error('Failed to save experiment', err); }
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// Poll session stats every 2 seconds
|
|
3054
|
+
sessionPollInterval = setInterval(pollSessionStats, 2000);
|
|
3055
|
+
pollSessionStats();
|
|
3056
|
+
|
|
1851
3057
|
// ============================================
|
|
1852
3058
|
// BOOT
|
|
1853
3059
|
// ============================================
|