@neuroverseos/nv-sim 0.1.4 → 0.1.5
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 +346 -68
- package/dist/adapters/mirofish.js +461 -0
- package/dist/adapters/scienceclaw.js +750 -0
- package/dist/assets/index-B64NuIXu.css +1 -0
- package/dist/assets/index-DbzSnYxr.js +532 -0
- package/dist/assets/{reportEngine-BfteK4MN.js → reportEngine-DKWTrP6-.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 +2787 -360
- package/dist/engine/metrics/science.metrics.js +335 -0
- package/dist/engine/narrativeInjection.js +55 -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/scenarioCapsule.js +56 -0
- package/dist/engine/scenarioComparison.js +463 -0
- package/dist/engine/scenarioLibrary.js +17 -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,270 @@ 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
|
+
// ── Bridge Metrics Tracker ──
|
|
190
|
+
// Computes meaningful live metrics from external /api/evaluate calls
|
|
191
|
+
let bridgeEvalCount = 0;
|
|
192
|
+
let bridgeBlockCount = 0;
|
|
193
|
+
let bridgeModifyCount = 0;
|
|
194
|
+
function computeBridgeMetrics(decision) {
|
|
195
|
+
bridgeEvalCount++;
|
|
196
|
+
if (decision === "BLOCK")
|
|
197
|
+
bridgeBlockCount++;
|
|
198
|
+
if (decision === "MODIFY")
|
|
199
|
+
bridgeModifyCount++;
|
|
200
|
+
const totalInterventions = bridgeBlockCount + bridgeModifyCount;
|
|
201
|
+
// Stability = ratio of non-blocked actions (higher = more stable)
|
|
202
|
+
const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount) / bridgeEvalCount : 1;
|
|
203
|
+
// Volatility = ratio of interventions (blocks + modifies) — more interventions = more volatile
|
|
204
|
+
const volatility = bridgeEvalCount > 0 ? totalInterventions / bridgeEvalCount : 0;
|
|
205
|
+
return { stability, volatility, totalInterventions, evalCount: bridgeEvalCount };
|
|
206
|
+
}
|
|
207
|
+
function synthesizeSessionReport() {
|
|
208
|
+
// Gather all sessions to report on (history + current if it has data)
|
|
209
|
+
const allSessions = [
|
|
210
|
+
...sessionHistory,
|
|
211
|
+
...(currentSession.evaluations.length > 0 ? [currentSession] : []),
|
|
212
|
+
];
|
|
213
|
+
if (allSessions.length === 0) {
|
|
214
|
+
return {
|
|
215
|
+
sessionId: currentSession.id,
|
|
216
|
+
scenario: "Live governance session (no evaluations yet)",
|
|
217
|
+
runs: [],
|
|
218
|
+
divergence: {
|
|
219
|
+
stabilityTrend: [],
|
|
220
|
+
collapseTrend: [],
|
|
221
|
+
effectivenessTrend: [],
|
|
222
|
+
bestIteration: 0,
|
|
223
|
+
worstIteration: 0,
|
|
224
|
+
totalDivergence: 0,
|
|
225
|
+
narrative: "No evaluations recorded. Send actions to POST /api/evaluate to begin.",
|
|
226
|
+
},
|
|
227
|
+
recommendation: "Start sending agent actions to /api/evaluate to generate governance data.",
|
|
228
|
+
generatedAt: new Date().toISOString(),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const runs = allSessions.map((sess, idx) => {
|
|
232
|
+
const evals = sess.evaluations;
|
|
233
|
+
const total = evals.length;
|
|
234
|
+
const blocked = evals.filter(e => e.decision === "BLOCK").length;
|
|
235
|
+
const modified = evals.filter(e => e.decision === "MODIFY").length;
|
|
236
|
+
const allowed = evals.filter(e => e.decision === "ALLOW").length;
|
|
237
|
+
// Synthesize metrics from real evaluation data
|
|
238
|
+
const blockRate = total > 0 ? blocked / total : 0;
|
|
239
|
+
const interventionRate = total > 0 ? (blocked + modified) / total : 0;
|
|
240
|
+
// Stability: higher when governance is actively catching harmful actions
|
|
241
|
+
// If nothing is blocked, either the system is clean OR governance is too weak
|
|
242
|
+
const stabilityScore = total > 0
|
|
243
|
+
? Math.min(0.95, 0.4 + interventionRate * 0.4 + (allowed / Math.max(1, total)) * 0.2)
|
|
244
|
+
: 0.5;
|
|
245
|
+
// Collapse probability: lower when more harmful actions are caught
|
|
246
|
+
const collapseProbability = total > 0
|
|
247
|
+
? Math.max(0.02, 0.6 - interventionRate * 0.5 - blockRate * 0.15)
|
|
248
|
+
: 0.5;
|
|
249
|
+
// Governance effectiveness: composite of intervention quality
|
|
250
|
+
const governanceEffectiveness = total > 0
|
|
251
|
+
? Math.min(0.95, interventionRate * 0.6 + blockRate * 0.3 + (total > 10 ? 0.1 : 0))
|
|
252
|
+
: 0;
|
|
253
|
+
// Unique rules that fired
|
|
254
|
+
const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
|
|
255
|
+
// Unique actors and actions
|
|
256
|
+
const uniqueActors = [...new Set(evals.map(e => e.actor))];
|
|
257
|
+
const uniqueActions = [...new Set(evals.map(e => e.action))];
|
|
258
|
+
return {
|
|
259
|
+
iteration: idx + 1,
|
|
260
|
+
worldName: sess.world || "live-session",
|
|
261
|
+
ruleCount: sess.guardCount,
|
|
262
|
+
gateCount: 0,
|
|
263
|
+
metrics: {
|
|
264
|
+
avgImpact: interventionRate > 0 ? -(interventionRate * 0.5) : 0.1,
|
|
265
|
+
collapseProbability,
|
|
266
|
+
stabilityScore,
|
|
267
|
+
coalitionRisks: 0,
|
|
268
|
+
polarizationEvents: 0,
|
|
269
|
+
peakNegativeSentiment: blockRate > 0.3 ? -0.6 : -0.2,
|
|
270
|
+
consensusRounds: 0,
|
|
271
|
+
maxVolatility: blockRate > 0.2 ? 0.7 : 0.3,
|
|
272
|
+
},
|
|
273
|
+
comparison: {
|
|
274
|
+
collapseReduction: collapseProbability < 0.5 ? (0.6 - collapseProbability) : 0,
|
|
275
|
+
stabilityImprovement: stabilityScore > 0.5 ? (stabilityScore - 0.4) : 0,
|
|
276
|
+
volatilityReduction: interventionRate * 0.5,
|
|
277
|
+
coalitionRiskReduction: 0,
|
|
278
|
+
governanceEffectiveness,
|
|
279
|
+
narrative: total > 0
|
|
280
|
+
? `${total} actions evaluated: ${blocked} blocked, ${modified} modified, ${allowed} allowed across ${uniqueActors.length} agent(s) performing ${uniqueActions.length} action type(s).`
|
|
281
|
+
: "No evaluations in this session.",
|
|
282
|
+
},
|
|
283
|
+
governanceStats: {
|
|
284
|
+
engineLoaded: true,
|
|
285
|
+
totalEvaluations: total,
|
|
286
|
+
verdicts: { allow: allowed, block: blocked, pause: modified },
|
|
287
|
+
rulesFired: triggeredRules.length,
|
|
288
|
+
worldCollapsed: false,
|
|
289
|
+
finalViability: stabilityScore > 0.6 ? "stable" : stabilityScore > 0.4 ? "at-risk" : "critical",
|
|
290
|
+
invariantsChecked: sess.guardCount,
|
|
291
|
+
triggeredGuards: triggeredRules,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
// Compute rule changes between sessions
|
|
296
|
+
for (let i = 1; i < runs.length; i++) {
|
|
297
|
+
const prev = allSessions[i - 1];
|
|
298
|
+
const curr = allSessions[i];
|
|
299
|
+
if (prev.guardCount !== curr.guardCount) {
|
|
300
|
+
runs[i].ruleChanges = {
|
|
301
|
+
added: [],
|
|
302
|
+
removed: [],
|
|
303
|
+
gatesAdded: [],
|
|
304
|
+
gatesRemoved: [],
|
|
305
|
+
thesisChanged: prev.world !== curr.world,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Divergence analysis
|
|
310
|
+
const stabilityTrend = runs.map(r => r.metrics.stabilityScore);
|
|
311
|
+
const collapseTrend = runs.map(r => r.metrics.collapseProbability);
|
|
312
|
+
const effectivenessTrend = runs.map(r => r.comparison.governanceEffectiveness);
|
|
313
|
+
let bestIteration = 1;
|
|
314
|
+
let worstIteration = 1;
|
|
315
|
+
let bestScore = -Infinity;
|
|
316
|
+
let worstScore = Infinity;
|
|
317
|
+
for (const run of runs) {
|
|
318
|
+
if (run.comparison.governanceEffectiveness > bestScore) {
|
|
319
|
+
bestScore = run.comparison.governanceEffectiveness;
|
|
320
|
+
bestIteration = run.iteration;
|
|
321
|
+
}
|
|
322
|
+
if (run.comparison.governanceEffectiveness < worstScore) {
|
|
323
|
+
worstScore = run.comparison.governanceEffectiveness;
|
|
324
|
+
worstIteration = run.iteration;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
let totalDivergence = 0;
|
|
328
|
+
for (let i = 1; i < runs.length; i++) {
|
|
329
|
+
totalDivergence += Math.abs(runs[i].metrics.stabilityScore - runs[i - 1].metrics.stabilityScore);
|
|
330
|
+
totalDivergence += Math.abs(runs[i].metrics.collapseProbability - runs[i - 1].metrics.collapseProbability);
|
|
331
|
+
totalDivergence += Math.abs(runs[i].comparison.governanceEffectiveness - runs[i - 1].comparison.governanceEffectiveness);
|
|
332
|
+
}
|
|
333
|
+
// Build narrative
|
|
334
|
+
const totalEvals = allSessions.reduce((sum, s) => sum + s.evaluations.length, 0);
|
|
335
|
+
const totalBlocked = allSessions.reduce((sum, s) => sum + s.evaluations.filter(e => e.decision === "BLOCK").length, 0);
|
|
336
|
+
const narrativeParts = [];
|
|
337
|
+
narrativeParts.push(`${allSessions.length} session(s), ${totalEvals} total evaluations, ${totalBlocked} blocked.`);
|
|
338
|
+
if (runs.length > 1) {
|
|
339
|
+
const stabDelta = stabilityTrend[stabilityTrend.length - 1] - stabilityTrend[0];
|
|
340
|
+
if (stabDelta > 0.05)
|
|
341
|
+
narrativeParts.push(`Stability improved ${(stabDelta * 100).toFixed(0)}pp across sessions.`);
|
|
342
|
+
narrativeParts.push(`Best outcome: session ${bestIteration}.`);
|
|
343
|
+
}
|
|
344
|
+
const best = runs[bestIteration - 1];
|
|
345
|
+
const recommendation = runs.length === 1
|
|
346
|
+
? `Single session: ${best.governanceStats.totalEvaluations} evaluations, ${best.governanceStats.verdicts.block} blocked. Apply different rules and reset to compare.`
|
|
347
|
+
: `Best session: #${bestIteration} ("${best.worldName}") — ${(best.comparison.governanceEffectiveness * 100).toFixed(0)}% effectiveness, ${(best.metrics.stabilityScore * 100).toFixed(0)}% stability.`;
|
|
348
|
+
return {
|
|
349
|
+
sessionId: currentSession.id,
|
|
350
|
+
scenario: `Live governance — ${currentSession.world}`,
|
|
351
|
+
runs,
|
|
352
|
+
divergence: {
|
|
353
|
+
stabilityTrend,
|
|
354
|
+
collapseTrend,
|
|
355
|
+
effectivenessTrend,
|
|
356
|
+
bestIteration,
|
|
357
|
+
worstIteration,
|
|
358
|
+
totalDivergence: Number(totalDivergence.toFixed(3)),
|
|
359
|
+
narrative: narrativeParts.join(" "),
|
|
360
|
+
},
|
|
361
|
+
recommendation,
|
|
362
|
+
generatedAt: new Date().toISOString(),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
103
365
|
function broadcast(event) {
|
|
104
366
|
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
105
367
|
for (const client of clients) {
|
|
@@ -133,6 +395,18 @@ function startInteractiveServer(port, onReady) {
|
|
|
133
395
|
});
|
|
134
396
|
world.state_variables = updatedVars;
|
|
135
397
|
}
|
|
398
|
+
// Inject custom rules as first-class invariants (not just guards)
|
|
399
|
+
// This ensures user rules affect BOTH governance paths:
|
|
400
|
+
// 1. Guard engine (intent-level blocking) — already wired via customGuards[]
|
|
401
|
+
// 2. Invariant engine (system-level stability shaping) — wired here
|
|
402
|
+
if (customGuards.length > 0) {
|
|
403
|
+
const customInvariants = customGuards.map(cg => ({
|
|
404
|
+
id: cg.id,
|
|
405
|
+
description: cg.description,
|
|
406
|
+
enforceable: cg.enforcement === "block",
|
|
407
|
+
}));
|
|
408
|
+
world.invariants = [...world.invariants, ...customInvariants];
|
|
409
|
+
}
|
|
136
410
|
// Resolve narrative events
|
|
137
411
|
let narrativeEvents = [];
|
|
138
412
|
if (config.scenarioId && scenarioLibrary_1.SCENARIO_LIBRARY[config.scenarioId]) {
|
|
@@ -156,7 +430,12 @@ function startInteractiveServer(port, onReady) {
|
|
|
156
430
|
scenario: resolved.scenario,
|
|
157
431
|
worldThesis: world.thesis,
|
|
158
432
|
agents: resolved.stakeholders.map(s => s.id),
|
|
159
|
-
invariants: world.invariants.map(inv => ({
|
|
433
|
+
invariants: world.invariants.map(inv => ({
|
|
434
|
+
id: inv.id,
|
|
435
|
+
description: inv.description,
|
|
436
|
+
enforcement: inv.enforceable ? "full" : "advisory",
|
|
437
|
+
source: inv.id.startsWith("custom-rule-") ? "user" : "world",
|
|
438
|
+
})),
|
|
160
439
|
gates: (world.gates ?? []).map(g => ({ id: g.id, label: g.label, severity: g.severity })),
|
|
161
440
|
narrativeEvents: narrativeEvents.map(e => ({ id: e.id, headline: e.headline, round: e.round, severity: e.severity })),
|
|
162
441
|
totalRounds: rounds,
|
|
@@ -238,7 +517,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
238
517
|
}
|
|
239
518
|
if (req.url === "/api/worlds" && req.method === "GET") {
|
|
240
519
|
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
241
|
-
const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis"];
|
|
520
|
+
const worldIds = ["trading", "strait_of_hormuz", "gas_price_spike", "ai_regulation_crisis", "science_research"];
|
|
242
521
|
const worlds = worldIds.map(id => {
|
|
243
522
|
try {
|
|
244
523
|
const r = resolveWorld(id);
|
|
@@ -372,47 +651,170 @@ function startInteractiveServer(port, onReady) {
|
|
|
372
651
|
// Without guards, everything defaults to ALLOW. These guards define what
|
|
373
652
|
// simulation actions should be governed.
|
|
374
653
|
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" },
|
|
654
|
+
// Detect world type from request — social media worlds get social guards,
|
|
655
|
+
// financial worlds get trading guards, unknown gets both.
|
|
656
|
+
const isSocial = payload.world === "social-media" || payload.world === "social"
|
|
657
|
+
|| ["create_post", "like_post", "repost", "follow", "unfollow", "create_comment",
|
|
658
|
+
"search_posts", "mute", "unmute", "trend"].includes(payload.action);
|
|
659
|
+
const financialGuards = [
|
|
660
|
+
{
|
|
661
|
+
id: "sim-panic-actions",
|
|
662
|
+
label: "Block panic-driven actions",
|
|
663
|
+
description: "Prevents panic selling, aggressive shorting, and other destabilizing actions during high volatility",
|
|
664
|
+
category: "structural",
|
|
665
|
+
enforcement: "block",
|
|
666
|
+
immutable: true,
|
|
667
|
+
invariant_ref: nvWorld.invariants[0]?.id,
|
|
668
|
+
intent_patterns: ["panic_sell", "panic sell", "panic buy", "panic_buy"],
|
|
669
|
+
default_enabled: true,
|
|
414
670
|
},
|
|
415
|
-
|
|
671
|
+
{
|
|
672
|
+
id: "sim-excessive-leverage",
|
|
673
|
+
label: "Block excessive leverage",
|
|
674
|
+
description: "Prevents increasing leverage positions that could amplify cascades",
|
|
675
|
+
category: "structural",
|
|
676
|
+
enforcement: "block",
|
|
677
|
+
immutable: true,
|
|
678
|
+
intent_patterns: ["increase_leverage", "increase leverage", "max leverage"],
|
|
679
|
+
default_enabled: true,
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
id: "sim-aggressive-actions",
|
|
683
|
+
label: "Pause aggressive market actions",
|
|
684
|
+
description: "Requires review for aggressive buying or shorting that could move markets",
|
|
685
|
+
category: "operational",
|
|
686
|
+
enforcement: "pause",
|
|
687
|
+
immutable: false,
|
|
688
|
+
intent_patterns: ["aggressive_buy", "aggressive buy", "aggressive_short", "short"],
|
|
689
|
+
default_enabled: true,
|
|
690
|
+
},
|
|
691
|
+
];
|
|
692
|
+
// Social media guards for MiroFish/OASIS agent actions.
|
|
693
|
+
// These govern what AI agents can do on simulated social platforms.
|
|
694
|
+
const socialGuards = [
|
|
695
|
+
{
|
|
696
|
+
id: "social-harmful-content",
|
|
697
|
+
label: "Block harmful content creation",
|
|
698
|
+
description: "Prevents agents from posting content that incites panic, spreads disinformation, or promotes harmful behavior",
|
|
699
|
+
category: "structural",
|
|
700
|
+
enforcement: "block",
|
|
701
|
+
immutable: true,
|
|
702
|
+
intent_patterns: ["create_post", "create_comment", "quote_post"],
|
|
703
|
+
content_patterns: ["panic", "crash", "collaps", "sell everything", "market is dead", "scam", "rug pull", "ponzi"],
|
|
704
|
+
default_enabled: true,
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
id: "social-coordinated-manipulation",
|
|
708
|
+
label: "Block coordinated manipulation",
|
|
709
|
+
description: "Prevents agents from engaging in coordinated inauthentic behavior like mass following, mass liking, or brigading",
|
|
710
|
+
category: "structural",
|
|
711
|
+
enforcement: "block",
|
|
712
|
+
immutable: true,
|
|
713
|
+
intent_patterns: ["follow", "like_post", "repost"],
|
|
714
|
+
default_enabled: true,
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
id: "social-spam-prevention",
|
|
718
|
+
label: "Pause high-frequency posting",
|
|
719
|
+
description: "Rate-limits agents that post too frequently, preventing spam and platform flooding",
|
|
720
|
+
category: "operational",
|
|
721
|
+
enforcement: "pause",
|
|
722
|
+
immutable: false,
|
|
723
|
+
intent_patterns: ["create_post", "create_comment", "repost", "quote_post"],
|
|
724
|
+
default_enabled: true,
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
id: "social-engagement-farming",
|
|
728
|
+
label: "Block engagement farming",
|
|
729
|
+
description: "Prevents agents from like-bombing, follow-unfollowing, or other engagement manipulation tactics",
|
|
730
|
+
category: "operational",
|
|
731
|
+
enforcement: "block",
|
|
732
|
+
immutable: false,
|
|
733
|
+
intent_patterns: ["like_post", "unlike_post", "follow", "unfollow"],
|
|
734
|
+
default_enabled: true,
|
|
735
|
+
},
|
|
736
|
+
];
|
|
737
|
+
const guards = isSocial ? socialGuards : financialGuards;
|
|
738
|
+
// Build intent vocabulary from selected guards
|
|
739
|
+
const intentVocabulary = {};
|
|
740
|
+
for (const g of guards) {
|
|
741
|
+
for (const pat of g.intent_patterns) {
|
|
742
|
+
if (!intentVocabulary[pat]) {
|
|
743
|
+
intentVocabulary[pat] = { label: pat.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()), pattern: pat };
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
nvWorld.guards = { guards, intent_vocabulary: intentVocabulary };
|
|
748
|
+
}
|
|
749
|
+
// Inject custom guards from plain-English rule editor
|
|
750
|
+
if (customGuards.length > 0 && nvWorld.guards) {
|
|
751
|
+
const existingGuards = nvWorld.guards.guards;
|
|
752
|
+
for (const cg of customGuards) {
|
|
753
|
+
existingGuards.push({
|
|
754
|
+
id: cg.id,
|
|
755
|
+
label: cg.label,
|
|
756
|
+
description: cg.description,
|
|
757
|
+
category: cg.category,
|
|
758
|
+
enforcement: cg.enforcement,
|
|
759
|
+
immutable: false,
|
|
760
|
+
intent_patterns: cg.intent_patterns,
|
|
761
|
+
default_enabled: true,
|
|
762
|
+
});
|
|
763
|
+
// Add patterns to vocabulary
|
|
764
|
+
for (const pat of cg.intent_patterns) {
|
|
765
|
+
nvWorld.guards.intent_vocabulary[pat] = { label: pat, pattern: pat };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Content-aware governance for social media actions.
|
|
770
|
+
// The guard engine matches intent against intent_patterns, but for social media
|
|
771
|
+
// we also need to check message content for harmful patterns.
|
|
772
|
+
const messageContent = typeof payload.payload?.message === "string"
|
|
773
|
+
? payload.payload.message.toLowerCase()
|
|
774
|
+
: typeof payload.payload?.content === "string"
|
|
775
|
+
? payload.payload.content.toLowerCase()
|
|
776
|
+
: "";
|
|
777
|
+
const contentPatterns = ["panic", "crash", "collaps", "sell everything", "market is dead",
|
|
778
|
+
"scam", "rug pull", "ponzi", "buy now or lose", "guaranteed returns", "get rich"];
|
|
779
|
+
const hasHarmfulContent = messageContent.length > 0
|
|
780
|
+
&& contentPatterns.some(p => messageContent.includes(p));
|
|
781
|
+
// If content is harmful and action is content-creating, short-circuit to BLOCK
|
|
782
|
+
const isContentAction = ["create_post", "create_comment", "quote_post", "repost"].includes(payload.action);
|
|
783
|
+
if (hasHarmfulContent && isContentAction) {
|
|
784
|
+
const matchedPattern = contentPatterns.find(p => messageContent.includes(p)) ?? "harmful content";
|
|
785
|
+
broadcast({
|
|
786
|
+
type: "round",
|
|
787
|
+
round: 0,
|
|
788
|
+
totalRounds: 0,
|
|
789
|
+
phase: "governed",
|
|
790
|
+
reactions: [{
|
|
791
|
+
stakeholder_id: payload.actor,
|
|
792
|
+
reaction: payload.action,
|
|
793
|
+
impact: 0,
|
|
794
|
+
confidence: 0.9,
|
|
795
|
+
trigger: "bridge",
|
|
796
|
+
verdict: { status: "BLOCK", reason: `Content violates governance: "${matchedPattern}" detected`, ruleId: "social-harmful-content" },
|
|
797
|
+
}],
|
|
798
|
+
avgImpact: 0,
|
|
799
|
+
maxVolatility: 0,
|
|
800
|
+
dynamics: [],
|
|
801
|
+
interventionCount: 1,
|
|
802
|
+
});
|
|
803
|
+
// Record in session
|
|
804
|
+
currentSession.evaluations.push({
|
|
805
|
+
actor: payload.actor, action: payload.action, decision: "BLOCK",
|
|
806
|
+
reason: `Content blocked: "${matchedPattern}" detected`,
|
|
807
|
+
ruleId: "social-harmful-content", world: payload.world ?? currentSession.world,
|
|
808
|
+
timestamp: Date.now(), payload: payload.payload,
|
|
809
|
+
});
|
|
810
|
+
jsonResponse(res, 200, {
|
|
811
|
+
decision: "BLOCK",
|
|
812
|
+
reason: `Content blocked: "${matchedPattern}" detected in ${payload.action} — violates social media governance policy`,
|
|
813
|
+
rule_id: "social-harmful-content",
|
|
814
|
+
evidence: { matched_pattern: matchedPattern, action: payload.action, actor: payload.actor },
|
|
815
|
+
modified_action: null,
|
|
816
|
+
});
|
|
817
|
+
return;
|
|
416
818
|
}
|
|
417
819
|
// Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
|
|
418
820
|
// Omit `direction` — setting it enables execution-intent safety checks (prompt injection
|
|
@@ -423,7 +825,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
423
825
|
tool: "simulation",
|
|
424
826
|
scope: `bridge/${payload.actor}`,
|
|
425
827
|
actionCategory: "execute",
|
|
426
|
-
riskLevel: (["panic_sell", "panic_buy", "increase_leverage"].includes(payload.action) ? "high" : "medium"),
|
|
828
|
+
riskLevel: (["panic_sell", "panic_buy", "increase_leverage", "create_post", "repost", "quote_post"].includes(payload.action) ? "high" : "medium"),
|
|
427
829
|
args: {
|
|
428
830
|
actor: payload.actor,
|
|
429
831
|
action: payload.action,
|
|
@@ -450,25 +852,60 @@ function startInteractiveServer(port, onReady) {
|
|
|
450
852
|
const decision = verdict.status === "BLOCK" ? "BLOCK"
|
|
451
853
|
: verdict.status === "PAUSE" ? "MODIFY"
|
|
452
854
|
: "ALLOW";
|
|
855
|
+
// Compute live metrics from cumulative bridge evaluations
|
|
856
|
+
const bridgeMetrics = computeBridgeMetrics(decision);
|
|
453
857
|
// Broadcast governance event to connected SSE clients
|
|
454
858
|
broadcast({
|
|
455
859
|
type: "round",
|
|
456
|
-
round:
|
|
860
|
+
round: bridgeMetrics.evalCount,
|
|
457
861
|
totalRounds: 0,
|
|
458
862
|
phase: "governed",
|
|
459
863
|
reactions: [{
|
|
460
864
|
stakeholder_id: payload.actor,
|
|
461
865
|
reaction: payload.action,
|
|
462
|
-
impact: 0,
|
|
866
|
+
impact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
|
|
463
867
|
confidence: 0.5,
|
|
464
868
|
trigger: "bridge",
|
|
465
869
|
verdict: { status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId },
|
|
466
870
|
}],
|
|
467
|
-
avgImpact: 0,
|
|
468
|
-
maxVolatility:
|
|
871
|
+
avgImpact: decision === "BLOCK" ? -0.8 : decision === "MODIFY" ? -0.3 : 0.1,
|
|
872
|
+
maxVolatility: bridgeMetrics.volatility,
|
|
469
873
|
dynamics: [],
|
|
470
874
|
interventionCount: decision !== "ALLOW" ? 1 : 0,
|
|
471
875
|
});
|
|
876
|
+
// Also broadcast a stability update so the metric refreshes live
|
|
877
|
+
broadcast({
|
|
878
|
+
type: "bridge_metrics",
|
|
879
|
+
stability: bridgeMetrics.stability,
|
|
880
|
+
volatility: bridgeMetrics.volatility,
|
|
881
|
+
evalCount: bridgeMetrics.evalCount,
|
|
882
|
+
totalInterventions: bridgeMetrics.totalInterventions,
|
|
883
|
+
});
|
|
884
|
+
// Record in session
|
|
885
|
+
currentSession.evaluations.push({
|
|
886
|
+
actor: payload.actor, action: payload.action,
|
|
887
|
+
decision: decision,
|
|
888
|
+
reason: verdict.reason ?? "", ruleId: verdict.ruleId ?? null,
|
|
889
|
+
world: payload.world ?? currentSession.world,
|
|
890
|
+
timestamp: Date.now(), payload: payload.payload,
|
|
891
|
+
});
|
|
892
|
+
currentSession.guardCount = customGuards.length + (nvWorld.guards?.guards?.length ?? 0);
|
|
893
|
+
// Persist to audit trail on disk
|
|
894
|
+
auditTrail.logVerdict({
|
|
895
|
+
agent: payload.actor,
|
|
896
|
+
action: payload.action,
|
|
897
|
+
actionType: payload.payload?.type ?? "unknown",
|
|
898
|
+
verdict: decision,
|
|
899
|
+
reason: verdict.reason ?? "",
|
|
900
|
+
confidence: verdict.confidence ?? 0.5,
|
|
901
|
+
rulesFired: verdict.ruleId ? [{
|
|
902
|
+
id: verdict.ruleId,
|
|
903
|
+
description: verdict.reason ?? "",
|
|
904
|
+
effect: decision === "BLOCK" ? "blocked" : decision === "MODIFY" ? "dampened" : "monitored",
|
|
905
|
+
impactReduction: decision === "BLOCK" ? 1 : decision === "MODIFY" ? 0.5 : 0,
|
|
906
|
+
}] : [],
|
|
907
|
+
worldState: payload.world ?? currentSession.world,
|
|
908
|
+
});
|
|
472
909
|
jsonResponse(res, 200, {
|
|
473
910
|
decision,
|
|
474
911
|
reason: verdict.reason ?? null,
|
|
@@ -478,6 +915,12 @@ function startInteractiveServer(port, onReady) {
|
|
|
478
915
|
});
|
|
479
916
|
}
|
|
480
917
|
catch (err) {
|
|
918
|
+
// Record fail-open in session (payload may be out of scope if JSON parse failed)
|
|
919
|
+
currentSession.evaluations.push({
|
|
920
|
+
actor: "unknown", action: "unknown",
|
|
921
|
+
decision: "ALLOW", reason: "Governance evaluation error — fail open",
|
|
922
|
+
ruleId: null, world: currentSession.world, timestamp: Date.now(),
|
|
923
|
+
});
|
|
481
924
|
// Fail open — return ALLOW on any error
|
|
482
925
|
jsonResponse(res, 200, {
|
|
483
926
|
decision: "ALLOW",
|
|
@@ -489,6 +932,399 @@ function startInteractiveServer(port, onReady) {
|
|
|
489
932
|
}
|
|
490
933
|
return;
|
|
491
934
|
}
|
|
935
|
+
// ── Plain-English Rule Parser ──
|
|
936
|
+
// Parses natural language rules into guard definitions (local, no LLM needed)
|
|
937
|
+
if (req.url === "/api/parse-rules" && req.method === "POST") {
|
|
938
|
+
try {
|
|
939
|
+
const body = await readBody(req);
|
|
940
|
+
const payload = JSON.parse(body);
|
|
941
|
+
if (!payload.text) {
|
|
942
|
+
jsonResponse(res, 400, { error: "text is required" });
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const lines = payload.text.split("\n").map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith("#"));
|
|
946
|
+
const rules = lines.map((line, i) => parseNaturalLanguageRule(line, i));
|
|
947
|
+
jsonResponse(res, 200, { rules: rules.filter(Boolean), raw: lines });
|
|
948
|
+
}
|
|
949
|
+
catch (err) {
|
|
950
|
+
jsonResponse(res, 400, { error: "Invalid request" });
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
// Apply parsed rules to the active governance context
|
|
955
|
+
if (req.url === "/api/apply-rules" && req.method === "POST") {
|
|
956
|
+
try {
|
|
957
|
+
const body = await readBody(req);
|
|
958
|
+
const payload = JSON.parse(body);
|
|
959
|
+
if (!payload.rules || !Array.isArray(payload.rules)) {
|
|
960
|
+
jsonResponse(res, 400, { error: "rules array is required" });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
// Store custom rules in memory for the session
|
|
964
|
+
customGuards.length = 0;
|
|
965
|
+
for (const rule of payload.rules) {
|
|
966
|
+
customGuards.push({
|
|
967
|
+
id: rule.id,
|
|
968
|
+
label: rule.description,
|
|
969
|
+
description: rule.description,
|
|
970
|
+
category: "custom",
|
|
971
|
+
enforcement: rule.enforcement,
|
|
972
|
+
immutable: false,
|
|
973
|
+
intent_patterns: rule.intent_patterns,
|
|
974
|
+
default_enabled: true,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
jsonResponse(res, 200, {
|
|
978
|
+
status: "applied",
|
|
979
|
+
applied: customGuards.length,
|
|
980
|
+
enforcement: "full",
|
|
981
|
+
detail: "Rules enforced across guard engine (intent blocking) AND invariant engine (system dynamics)",
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
jsonResponse(res, 400, { error: "Invalid request" });
|
|
986
|
+
}
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
// ── Clear Rules ──
|
|
990
|
+
// Wipes all custom guards and resets governance to base world rules
|
|
991
|
+
if (req.url === "/api/clear-rules" && req.method === "POST") {
|
|
992
|
+
customGuards.length = 0;
|
|
993
|
+
jsonResponse(res, 200, {
|
|
994
|
+
status: "cleared",
|
|
995
|
+
message: "All custom rules removed. Governance reset to base world rules.",
|
|
996
|
+
});
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
// ── Load World File ──
|
|
1000
|
+
// Accept a full world definition JSON and use it as the active world
|
|
1001
|
+
if (req.url === "/api/load-world-file" && req.method === "POST") {
|
|
1002
|
+
try {
|
|
1003
|
+
const body = await readBody(req);
|
|
1004
|
+
const payload = JSON.parse(body);
|
|
1005
|
+
if (!payload.world) {
|
|
1006
|
+
jsonResponse(res, 400, { error: "world object is required" });
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const w = payload.world;
|
|
1010
|
+
// Parse any plain-English rules in the world file into guards
|
|
1011
|
+
customGuards.length = 0;
|
|
1012
|
+
if (w.rules && Array.isArray(w.rules)) {
|
|
1013
|
+
for (let i = 0; i < w.rules.length; i++) {
|
|
1014
|
+
const rule = w.rules[i];
|
|
1015
|
+
if (rule.intent_patterns && rule.intent_patterns.length > 0) {
|
|
1016
|
+
// Already structured rule
|
|
1017
|
+
customGuards.push({
|
|
1018
|
+
id: rule.id || `world-rule-${i}`,
|
|
1019
|
+
label: rule.description,
|
|
1020
|
+
description: rule.description,
|
|
1021
|
+
category: "world-file",
|
|
1022
|
+
enforcement: rule.enforcement || "block",
|
|
1023
|
+
immutable: false,
|
|
1024
|
+
intent_patterns: rule.intent_patterns,
|
|
1025
|
+
default_enabled: true,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
// Plain-English rule — parse it
|
|
1030
|
+
const parsed = parseNaturalLanguageRule(rule.description, i);
|
|
1031
|
+
if (parsed) {
|
|
1032
|
+
customGuards.push({
|
|
1033
|
+
id: parsed.id,
|
|
1034
|
+
label: parsed.description,
|
|
1035
|
+
description: parsed.description,
|
|
1036
|
+
category: "world-file",
|
|
1037
|
+
enforcement: parsed.enforcement,
|
|
1038
|
+
immutable: false,
|
|
1039
|
+
intent_patterns: parsed.intent_patterns,
|
|
1040
|
+
default_enabled: true,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
// Build the response world definition for the UI
|
|
1047
|
+
const loadedWorld = {
|
|
1048
|
+
id: "custom-world",
|
|
1049
|
+
title: w.name || "Custom World",
|
|
1050
|
+
thesis: w.thesis || "User-defined world",
|
|
1051
|
+
stateVariables: w.state_variables || [],
|
|
1052
|
+
invariants: (w.invariants || []).map(inv => ({
|
|
1053
|
+
id: inv.id,
|
|
1054
|
+
description: inv.description,
|
|
1055
|
+
enforceable: inv.enforceable !== false,
|
|
1056
|
+
})),
|
|
1057
|
+
gates: (w.gates || []).map(g => ({
|
|
1058
|
+
id: g.id,
|
|
1059
|
+
label: g.label,
|
|
1060
|
+
condition: g.condition,
|
|
1061
|
+
severity: g.severity || "warning",
|
|
1062
|
+
})),
|
|
1063
|
+
rulesApplied: customGuards.length,
|
|
1064
|
+
};
|
|
1065
|
+
jsonResponse(res, 200, {
|
|
1066
|
+
status: "loaded",
|
|
1067
|
+
world: loadedWorld,
|
|
1068
|
+
rulesApplied: customGuards.length,
|
|
1069
|
+
message: `World "${loadedWorld.title}" loaded with ${loadedWorld.invariants.length} invariants, ${loadedWorld.gates.length} gates, and ${customGuards.length} rules.`,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
catch (err) {
|
|
1073
|
+
jsonResponse(res, 400, { error: "Invalid world file JSON" });
|
|
1074
|
+
}
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
// ── Export World File ──
|
|
1078
|
+
// Exports the current world configuration (base world + custom rules + overrides) as a world file
|
|
1079
|
+
if (req.url === "/api/export-world" && req.method === "GET") {
|
|
1080
|
+
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
1081
|
+
const worldId = currentSession.world || "trading";
|
|
1082
|
+
let baseWorld;
|
|
1083
|
+
try {
|
|
1084
|
+
baseWorld = resolveWorld(worldId);
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
baseWorld = null;
|
|
1088
|
+
}
|
|
1089
|
+
const exportedWorld = {
|
|
1090
|
+
name: baseWorld?.title || worldId,
|
|
1091
|
+
thesis: baseWorld?.world?.thesis || "Exported world",
|
|
1092
|
+
state_variables: baseWorld?.world?.state_variables || [],
|
|
1093
|
+
invariants: baseWorld?.world?.invariants || [],
|
|
1094
|
+
gates: baseWorld?.world?.gates || [],
|
|
1095
|
+
rules: customGuards.map(g => ({
|
|
1096
|
+
id: g.id,
|
|
1097
|
+
description: g.description,
|
|
1098
|
+
enforcement: g.enforcement,
|
|
1099
|
+
intent_patterns: g.intent_patterns,
|
|
1100
|
+
})),
|
|
1101
|
+
};
|
|
1102
|
+
res.writeHead(200, {
|
|
1103
|
+
"Content-Type": "application/json",
|
|
1104
|
+
"Content-Disposition": `attachment; filename="${worldId}-world.json"`,
|
|
1105
|
+
"Access-Control-Allow-Origin": "*",
|
|
1106
|
+
});
|
|
1107
|
+
res.end(JSON.stringify({ world: exportedWorld }, null, 2));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
// ── Session Reporting Endpoints ──
|
|
1111
|
+
// Connect the serve runtime to the enforce reporting pipeline.
|
|
1112
|
+
// Users can request reports, stats, and recommendations from live governance data.
|
|
1113
|
+
// GET /api/session — current session stats (lightweight)
|
|
1114
|
+
if (req.url === "/api/session" && req.method === "GET") {
|
|
1115
|
+
const evals = currentSession.evaluations;
|
|
1116
|
+
const blocked = evals.filter(e => e.decision === "BLOCK").length;
|
|
1117
|
+
const modified = evals.filter(e => e.decision === "MODIFY").length;
|
|
1118
|
+
const allowed = evals.filter(e => e.decision === "ALLOW").length;
|
|
1119
|
+
const uniqueActors = [...new Set(evals.map(e => e.actor))];
|
|
1120
|
+
const uniqueActions = [...new Set(evals.map(e => e.action))];
|
|
1121
|
+
const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
|
|
1122
|
+
jsonResponse(res, 200, {
|
|
1123
|
+
sessionId: currentSession.id,
|
|
1124
|
+
startedAt: currentSession.startedAt,
|
|
1125
|
+
world: currentSession.world,
|
|
1126
|
+
guardCount: currentSession.guardCount,
|
|
1127
|
+
evaluations: {
|
|
1128
|
+
total: evals.length,
|
|
1129
|
+
blocked,
|
|
1130
|
+
modified,
|
|
1131
|
+
allowed,
|
|
1132
|
+
},
|
|
1133
|
+
agents: uniqueActors,
|
|
1134
|
+
actionTypes: uniqueActions,
|
|
1135
|
+
triggeredRules,
|
|
1136
|
+
historyCount: sessionHistory.length,
|
|
1137
|
+
});
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
// GET /api/session/report — full enforcement report (text)
|
|
1141
|
+
if (req.url === "/api/session/report" && req.method === "GET") {
|
|
1142
|
+
const report = synthesizeSessionReport();
|
|
1143
|
+
// Format as human-readable text using the same style as enforce CLI
|
|
1144
|
+
const lines = [];
|
|
1145
|
+
lines.push("");
|
|
1146
|
+
lines.push(" LIVE GOVERNANCE — ENFORCEMENT REPORT");
|
|
1147
|
+
lines.push(" " + "=".repeat(70));
|
|
1148
|
+
lines.push(` Session: ${report.sessionId}`);
|
|
1149
|
+
lines.push(` Scenario: ${report.scenario}`);
|
|
1150
|
+
lines.push(` Sessions: ${report.runs.length}`);
|
|
1151
|
+
lines.push(` Generated: ${report.generatedAt}`);
|
|
1152
|
+
lines.push("");
|
|
1153
|
+
if (report.runs.length > 0) {
|
|
1154
|
+
lines.push(" RUN HISTORY");
|
|
1155
|
+
lines.push(" " + "-".repeat(70));
|
|
1156
|
+
lines.push(` ${"#".padEnd(4)} ${"World".padEnd(25)} ${"Rules".padEnd(8)} ${"Evals".padEnd(8)} ${"Blocked".padEnd(10)} ${"Effectiveness"}`);
|
|
1157
|
+
lines.push(" " + "-".repeat(70));
|
|
1158
|
+
for (const run of report.runs) {
|
|
1159
|
+
const name = run.worldName.length > 23 ? run.worldName.slice(0, 22) + "…" : run.worldName;
|
|
1160
|
+
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)}%`);
|
|
1161
|
+
}
|
|
1162
|
+
lines.push("");
|
|
1163
|
+
lines.push(" DIVERGENCE ANALYSIS");
|
|
1164
|
+
lines.push(" " + "-".repeat(70));
|
|
1165
|
+
lines.push(` Stability trend: ${report.divergence.stabilityTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1166
|
+
lines.push(` Collapse trend: ${report.divergence.collapseTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1167
|
+
lines.push(` Effectiveness trend: ${report.divergence.effectivenessTrend.map(s => `${(s * 100).toFixed(0)}%`).join(" → ")}`);
|
|
1168
|
+
lines.push(` Total divergence: ${report.divergence.totalDivergence}`);
|
|
1169
|
+
lines.push("");
|
|
1170
|
+
lines.push(` ${report.divergence.narrative}`);
|
|
1171
|
+
// Blocked action breakdown
|
|
1172
|
+
const allEvals = [
|
|
1173
|
+
...sessionHistory.flatMap(s => s.evaluations),
|
|
1174
|
+
...currentSession.evaluations,
|
|
1175
|
+
];
|
|
1176
|
+
const blockedEvals = allEvals.filter(e => e.decision === "BLOCK");
|
|
1177
|
+
if (blockedEvals.length > 0) {
|
|
1178
|
+
lines.push("");
|
|
1179
|
+
lines.push(" BLOCKED ACTIONS");
|
|
1180
|
+
lines.push(" " + "-".repeat(70));
|
|
1181
|
+
// Group by rule
|
|
1182
|
+
const byRule = {};
|
|
1183
|
+
for (const e of blockedEvals) {
|
|
1184
|
+
const key = e.ruleId ?? "unknown";
|
|
1185
|
+
if (!byRule[key])
|
|
1186
|
+
byRule[key] = { count: 0, actors: new Set(), actions: new Set() };
|
|
1187
|
+
byRule[key].count++;
|
|
1188
|
+
byRule[key].actors.add(e.actor);
|
|
1189
|
+
byRule[key].actions.add(e.action);
|
|
1190
|
+
}
|
|
1191
|
+
const sorted = Object.entries(byRule).sort((a, b) => b[1].count - a[1].count);
|
|
1192
|
+
for (const [ruleId, data] of sorted) {
|
|
1193
|
+
lines.push(` ${ruleId}: ${data.count} blocks across ${data.actors.size} agent(s)`);
|
|
1194
|
+
lines.push(` Actions: ${[...data.actions].join(", ")}`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
// Recommendations (deterministic, no LLM needed)
|
|
1198
|
+
lines.push("");
|
|
1199
|
+
lines.push(" RECOMMENDATIONS");
|
|
1200
|
+
lines.push(" " + "-".repeat(70));
|
|
1201
|
+
const best = report.runs[report.divergence.bestIteration - 1];
|
|
1202
|
+
if (best) {
|
|
1203
|
+
const totalEvals = best.governanceStats.totalEvaluations;
|
|
1204
|
+
const blockRate = totalEvals > 0 ? best.governanceStats.verdicts.block / totalEvals : 0;
|
|
1205
|
+
if (blockRate > 0.5) {
|
|
1206
|
+
lines.push(" HIGH BLOCK RATE — Rules may be too aggressive.");
|
|
1207
|
+
lines.push(" Try relaxing constraints or adding MODIFY actions instead of hard blocks.");
|
|
1208
|
+
lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
|
|
1209
|
+
}
|
|
1210
|
+
else if (blockRate < 0.05 && totalEvals > 20) {
|
|
1211
|
+
lines.push(" LOW BLOCK RATE — Rules may be too permissive.");
|
|
1212
|
+
lines.push(" Try adding content-specific guards or lowering thresholds.");
|
|
1213
|
+
lines.push(` Current block rate: ${(blockRate * 100).toFixed(0)}% (${best.governanceStats.verdicts.block}/${totalEvals})`);
|
|
1214
|
+
}
|
|
1215
|
+
else if (blockRate > 0) {
|
|
1216
|
+
lines.push(` Governance is active: ${(blockRate * 100).toFixed(0)}% of actions blocked.`);
|
|
1217
|
+
lines.push(" To iterate: apply different rules via POST /api/apply-rules, then POST /api/session/reset.");
|
|
1218
|
+
lines.push(" This creates a new session for side-by-side comparison.");
|
|
1219
|
+
}
|
|
1220
|
+
if (report.runs.length === 1) {
|
|
1221
|
+
lines.push("");
|
|
1222
|
+
lines.push(" EXPERIMENT: Apply different rules and reset to compare sessions.");
|
|
1223
|
+
lines.push(" This will show divergence: how different governance changes outcomes.");
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
lines.push("");
|
|
1227
|
+
lines.push(" RECOMMENDATION");
|
|
1228
|
+
lines.push(" " + "-".repeat(70));
|
|
1229
|
+
lines.push(` ${report.recommendation}`);
|
|
1230
|
+
}
|
|
1231
|
+
lines.push("");
|
|
1232
|
+
lines.push(" " + "=".repeat(70));
|
|
1233
|
+
lines.push(" NeuroVerse Policy Enforcement System — Live Governance");
|
|
1234
|
+
lines.push(" Design rules. Run reality. See what changes.");
|
|
1235
|
+
lines.push(" " + "=".repeat(70));
|
|
1236
|
+
lines.push("");
|
|
1237
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
1238
|
+
res.end(lines.join("\n"));
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
// GET /api/session/report.json — full enforcement report (JSON)
|
|
1242
|
+
if (req.url === "/api/session/report.json" && req.method === "GET") {
|
|
1243
|
+
const report = synthesizeSessionReport();
|
|
1244
|
+
jsonResponse(res, 200, report);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
// GET /api/session/evaluations — raw evaluation log
|
|
1248
|
+
if (req.url === "/api/session/evaluations" && req.method === "GET") {
|
|
1249
|
+
jsonResponse(res, 200, {
|
|
1250
|
+
sessionId: currentSession.id,
|
|
1251
|
+
count: currentSession.evaluations.length,
|
|
1252
|
+
evaluations: currentSession.evaluations.slice(-200), // last 200
|
|
1253
|
+
});
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
// POST /api/session/reset — snapshot current session and start fresh
|
|
1257
|
+
// This is how users create multi-run comparisons:
|
|
1258
|
+
// 1. Apply rules A → run simulation → POST /api/session/reset
|
|
1259
|
+
// 2. Apply rules B → run simulation → GET /api/session/report
|
|
1260
|
+
// Now the report shows divergence between rules A and rules B.
|
|
1261
|
+
if (req.url === "/api/session/reset" && req.method === "POST") {
|
|
1262
|
+
if (currentSession.evaluations.length > 0) {
|
|
1263
|
+
sessionHistory.push({ ...currentSession, evaluations: [...currentSession.evaluations] });
|
|
1264
|
+
}
|
|
1265
|
+
const body = await readBody(req).catch(() => "{}");
|
|
1266
|
+
const opts = JSON.parse(body || "{}");
|
|
1267
|
+
currentSession = {
|
|
1268
|
+
id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
1269
|
+
startedAt: new Date().toISOString(),
|
|
1270
|
+
world: opts.world ?? currentSession.world,
|
|
1271
|
+
guardCount: customGuards.length,
|
|
1272
|
+
evaluations: [],
|
|
1273
|
+
};
|
|
1274
|
+
// Reset bridge metrics counters
|
|
1275
|
+
bridgeEvalCount = 0;
|
|
1276
|
+
bridgeBlockCount = 0;
|
|
1277
|
+
bridgeModifyCount = 0;
|
|
1278
|
+
jsonResponse(res, 200, {
|
|
1279
|
+
status: "reset",
|
|
1280
|
+
newSessionId: currentSession.id,
|
|
1281
|
+
previousSessions: sessionHistory.length,
|
|
1282
|
+
message: `Session reset. ${sessionHistory.length} session(s) in history. Apply new rules and evaluate to compare.`,
|
|
1283
|
+
});
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
// POST /api/session/save — save session as experiment with lineage
|
|
1287
|
+
if (req.url === "/api/session/save" && req.method === "POST") {
|
|
1288
|
+
const report = synthesizeSessionReport();
|
|
1289
|
+
if (report.runs.length === 0) {
|
|
1290
|
+
jsonResponse(res, 400, { error: "No evaluations to save. Send actions to /api/evaluate first." });
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const experiment = {
|
|
1294
|
+
id: `exp-live-${Date.now().toString(36)}`,
|
|
1295
|
+
savedAt: new Date().toISOString(),
|
|
1296
|
+
scenario: report.scenario,
|
|
1297
|
+
source: "live-governance",
|
|
1298
|
+
sessions: report.runs.length,
|
|
1299
|
+
totalEvaluations: report.runs.reduce((sum, r) => sum + r.governanceStats.totalEvaluations, 0),
|
|
1300
|
+
metrics: {
|
|
1301
|
+
stability: report.runs[report.divergence.bestIteration - 1]?.metrics.stabilityScore ?? 0,
|
|
1302
|
+
effectiveness: report.runs[report.divergence.bestIteration - 1]?.comparison.governanceEffectiveness ?? 0,
|
|
1303
|
+
collapseProbability: report.runs[report.divergence.bestIteration - 1]?.metrics.collapseProbability ?? 0,
|
|
1304
|
+
},
|
|
1305
|
+
divergence: report.divergence,
|
|
1306
|
+
recommendation: report.recommendation,
|
|
1307
|
+
report,
|
|
1308
|
+
};
|
|
1309
|
+
// Save to experiments/ directory
|
|
1310
|
+
try {
|
|
1311
|
+
if (!fs.existsSync("experiments"))
|
|
1312
|
+
fs.mkdirSync("experiments", { recursive: true });
|
|
1313
|
+
const filePath = path.join("experiments", `${experiment.id}.json`);
|
|
1314
|
+
fs.writeFileSync(filePath, JSON.stringify(experiment, null, 2));
|
|
1315
|
+
jsonResponse(res, 200, {
|
|
1316
|
+
status: "saved",
|
|
1317
|
+
experimentId: experiment.id,
|
|
1318
|
+
filePath,
|
|
1319
|
+
metrics: experiment.metrics,
|
|
1320
|
+
message: `Saved to ${filePath}. View with: npx nv-sim enforce --load ${filePath}`,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
catch (err) {
|
|
1324
|
+
jsonResponse(res, 500, { error: "Failed to save experiment", detail: String(err) });
|
|
1325
|
+
}
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
492
1328
|
// List available live adapters
|
|
493
1329
|
if (req.url === "/api/adapters" && req.method === "GET") {
|
|
494
1330
|
const adapters = Object.values(liveAdapter_1.ADAPTER_REGISTRY).map(a => ({
|
|
@@ -616,6 +1452,81 @@ function startInteractiveServer(port, onReady) {
|
|
|
616
1452
|
}
|
|
617
1453
|
return;
|
|
618
1454
|
}
|
|
1455
|
+
// ── AUDIT TRAIL ENDPOINTS ──
|
|
1456
|
+
// GET /api/audit — summary of current session's audit trail
|
|
1457
|
+
if (req.url === "/api/audit" && req.method === "GET") {
|
|
1458
|
+
jsonResponse(res, 200, auditTrail.summary());
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
// GET /api/audit/entries — all entries from current session (with optional filters)
|
|
1462
|
+
if (req.url?.startsWith("/api/audit/entries") && req.method === "GET") {
|
|
1463
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1464
|
+
const entries = auditTrail.query({
|
|
1465
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1466
|
+
agent: url.searchParams.get("agent") ?? undefined,
|
|
1467
|
+
verdict: url.searchParams.get("verdict") ?? undefined,
|
|
1468
|
+
runId: url.searchParams.get("runId") ?? undefined,
|
|
1469
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
1470
|
+
before: url.searchParams.get("before") ?? undefined,
|
|
1471
|
+
});
|
|
1472
|
+
jsonResponse(res, 200, {
|
|
1473
|
+
sessionId: auditTrail.getSessionId(),
|
|
1474
|
+
count: entries.length,
|
|
1475
|
+
entries,
|
|
1476
|
+
});
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
// GET /api/audit/entries/text — human-readable audit trail
|
|
1480
|
+
if (req.url?.startsWith("/api/audit/entries/text") && req.method === "GET") {
|
|
1481
|
+
const entries = auditTrail.readAll();
|
|
1482
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
1483
|
+
res.end((0, auditTrace_1.formatAuditTrail)(entries, { verbose: true }));
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
// GET /api/audit/sessions — list all audit sessions on disk
|
|
1487
|
+
if (req.url === "/api/audit/sessions" && req.method === "GET") {
|
|
1488
|
+
const sessions = (0, auditTrace_1.listAuditSessions)();
|
|
1489
|
+
jsonResponse(res, 200, {
|
|
1490
|
+
sessions: sessions.map((id) => {
|
|
1491
|
+
const trail = (0, auditTrace_1.loadAuditSession)(id);
|
|
1492
|
+
return trail.summary();
|
|
1493
|
+
}),
|
|
1494
|
+
});
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
// GET /api/audit/sessions/:id — load a specific session's audit trail
|
|
1498
|
+
const auditSessionMatch = req.url?.match(/^\/api\/audit\/sessions\/([^/]+)$/);
|
|
1499
|
+
if (auditSessionMatch && req.method === "GET") {
|
|
1500
|
+
const sessionId = decodeURIComponent(auditSessionMatch[1]);
|
|
1501
|
+
const trail = (0, auditTrace_1.loadAuditSession)(sessionId);
|
|
1502
|
+
const entries = trail.readAll();
|
|
1503
|
+
if (entries.length === 0) {
|
|
1504
|
+
jsonResponse(res, 404, { error: `Audit session "${sessionId}" not found or empty` });
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
jsonResponse(res, 200, {
|
|
1508
|
+
...trail.summary(),
|
|
1509
|
+
entries,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
// GET /api/audit/search — search across ALL sessions
|
|
1515
|
+
if (req.url?.startsWith("/api/audit/search") && req.method === "GET") {
|
|
1516
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
1517
|
+
const results = (0, auditTrace_1.searchAuditTrails)({
|
|
1518
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1519
|
+
agent: url.searchParams.get("agent") ?? undefined,
|
|
1520
|
+
verdict: url.searchParams.get("verdict") ?? undefined,
|
|
1521
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
1522
|
+
before: url.searchParams.get("before") ?? undefined,
|
|
1523
|
+
});
|
|
1524
|
+
jsonResponse(res, 200, {
|
|
1525
|
+
count: results.length,
|
|
1526
|
+
entries: results,
|
|
1527
|
+
});
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
619
1530
|
// Serve the interactive dashboard
|
|
620
1531
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
621
1532
|
res.end(INTERACTIVE_DASHBOARD_HTML);
|
|
@@ -632,251 +1543,611 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
632
1543
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
633
1544
|
<title>NV-SIM — Scenario Control Platform</title>
|
|
634
1545
|
<style>
|
|
1546
|
+
/* ── Theme Variables ── */
|
|
1547
|
+
:root {
|
|
1548
|
+
--bg-primary: #0f0f0f;
|
|
1549
|
+
--bg-secondary: #141414;
|
|
1550
|
+
--bg-surface: #1a1a1a;
|
|
1551
|
+
--bg-elevated: #222;
|
|
1552
|
+
--border: #2a2a2a;
|
|
1553
|
+
--border-subtle: #333;
|
|
1554
|
+
--text-primary: #f0f0f0;
|
|
1555
|
+
--text-secondary: #b0b0b0;
|
|
1556
|
+
--text-muted: #888;
|
|
1557
|
+
--text-faint: #666;
|
|
1558
|
+
--accent: #818cf8;
|
|
1559
|
+
--accent-bg: #1e1e3a;
|
|
1560
|
+
--green: #4ade80;
|
|
1561
|
+
--green-bg: #0a2a14;
|
|
1562
|
+
--red: #f87171;
|
|
1563
|
+
--red-bg: #2d0a0a;
|
|
1564
|
+
--yellow: #fbbf24;
|
|
1565
|
+
--yellow-bg: #2d2006;
|
|
1566
|
+
--blue: #60a5fa;
|
|
1567
|
+
--blue-bg: #1e293b;
|
|
1568
|
+
--purple: #a78bfa;
|
|
1569
|
+
}
|
|
1570
|
+
body.light {
|
|
1571
|
+
--bg-primary: #f5f5f5;
|
|
1572
|
+
--bg-secondary: #eaeaea;
|
|
1573
|
+
--bg-surface: #e0e0e0;
|
|
1574
|
+
--bg-elevated: #d4d4d4;
|
|
1575
|
+
--border: #c0c0c0;
|
|
1576
|
+
--border-subtle: #b0b0b0;
|
|
1577
|
+
--text-primary: #1a1a1a;
|
|
1578
|
+
--text-secondary: #444;
|
|
1579
|
+
--text-muted: #666;
|
|
1580
|
+
--text-faint: #888;
|
|
1581
|
+
--accent: #6366f1;
|
|
1582
|
+
--accent-bg: #e8e8ff;
|
|
1583
|
+
--green: #16a34a;
|
|
1584
|
+
--green-bg: #dcfce7;
|
|
1585
|
+
--red: #dc2626;
|
|
1586
|
+
--red-bg: #fee2e2;
|
|
1587
|
+
--yellow: #ca8a04;
|
|
1588
|
+
--yellow-bg: #fef9c3;
|
|
1589
|
+
--blue: #2563eb;
|
|
1590
|
+
--blue-bg: #dbeafe;
|
|
1591
|
+
--purple: #7c3aed;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
635
1594
|
* { 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:
|
|
1595
|
+
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; }
|
|
1596
|
+
.header { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
1597
|
+
.header h1 { font-size: 15px; color: var(--text-primary); }
|
|
1598
|
+
.header .sub { font-size: 11px; color: var(--text-muted); margin-left: 12px; }
|
|
1599
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
1600
|
+
.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; }
|
|
1601
|
+
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
|
640
1602
|
.status { font-size: 11px; padding: 3px 10px; border-radius: 10px; }
|
|
641
|
-
.status.idle { background:
|
|
642
|
-
.status.live { background:
|
|
643
|
-
.status.complete { background:
|
|
1603
|
+
.status.idle { background: var(--accent-bg); color: var(--accent); }
|
|
1604
|
+
.status.live { background: var(--green-bg); color: var(--green); animation: pulse 2s infinite; }
|
|
1605
|
+
.status.complete { background: var(--accent-bg); color: var(--accent); }
|
|
644
1606
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
645
1607
|
|
|
646
1608
|
.layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }
|
|
647
1609
|
|
|
648
1610
|
/* LEFT PANEL — Controls */
|
|
649
|
-
.controls { background:
|
|
1611
|
+
.controls { background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px; }
|
|
650
1612
|
.ctrl-section { margin-bottom: 20px; }
|
|
651
|
-
.ctrl-section h3 { font-size: 11px; color:
|
|
1613
|
+
.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
1614
|
|
|
653
1615
|
.ctrl-row { margin-bottom: 12px; }
|
|
654
|
-
.ctrl-label { font-size: 11px; color:
|
|
655
|
-
.ctrl-label .val { color:
|
|
1616
|
+
.ctrl-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; display: flex; justify-content: space-between; }
|
|
1617
|
+
.ctrl-label .val { color: var(--text-primary); font-weight: 600; }
|
|
656
1618
|
|
|
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:
|
|
1619
|
+
input[type="range"] { width: 100%; height: 4px; -webkit-appearance: none; background: var(--bg-elevated); border-radius: 2px; outline: none; }
|
|
1620
|
+
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; }
|
|
1621
|
+
input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: none; }
|
|
660
1622
|
|
|
661
|
-
select { width: 100%; background:
|
|
1623
|
+
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
1624
|
|
|
663
1625
|
.toggle-row { display: flex; align-items: center; gap: 8px; }
|
|
664
|
-
.toggle { position: relative; width: 36px; height: 20px; background:
|
|
665
|
-
.toggle.on { background:
|
|
1626
|
+
.toggle { position: relative; width: 36px; height: 20px; background: var(--border-subtle); border-radius: 10px; cursor: pointer; transition: background 0.2s; }
|
|
1627
|
+
.toggle.on { background: var(--green); }
|
|
666
1628
|
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
|
|
667
1629
|
.toggle.on::after { transform: translateX(16px); }
|
|
668
|
-
.toggle-label { font-size: 12px; color:
|
|
1630
|
+
.toggle-label { font-size: 12px; color: var(--text-secondary); }
|
|
669
1631
|
|
|
670
1632
|
.inject-row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
671
1633
|
.inject-row select { flex: 1; }
|
|
672
|
-
.inject-row input { width: 50px; background:
|
|
1634
|
+
.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
1635
|
.inject-list { margin-bottom: 8px; }
|
|
674
|
-
.inject-item { font-size: 11px; color:
|
|
675
|
-
.inject-item .remove { color:
|
|
1636
|
+
.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; }
|
|
1637
|
+
.inject-item .remove { color: var(--red); cursor: pointer; }
|
|
676
1638
|
|
|
677
1639
|
.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:
|
|
1640
|
+
.btn-run { background: var(--green); color: #0a0a0a; }
|
|
679
1641
|
.btn-run:hover { background: #22c55e; }
|
|
680
|
-
.btn-run:disabled { background:
|
|
681
|
-
.btn-add { background:
|
|
682
|
-
.btn-add:hover { background:
|
|
1642
|
+
.btn-run:disabled { background: var(--bg-elevated); color: var(--text-faint); cursor: not-allowed; }
|
|
1643
|
+
.btn-add { background: var(--bg-elevated); color: var(--accent); padding: 6px; font-size: 11px; }
|
|
1644
|
+
.btn-add:hover { background: var(--accent-bg); }
|
|
683
1645
|
|
|
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:
|
|
1646
|
+
.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; }
|
|
1647
|
+
.scenario-btn:hover { border-color: var(--accent); background: var(--accent-bg); }
|
|
1648
|
+
.scenario-btn .stitle { font-weight: 600; color: var(--text-primary); }
|
|
1649
|
+
.scenario-btn .sdesc { color: var(--text-muted); font-size: 10px; margin-top: 2px; }
|
|
688
1650
|
|
|
689
1651
|
/* 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:
|
|
1652
|
+
.btn-save { background: var(--accent-bg); color: var(--accent); margin-top: 0; }
|
|
1653
|
+
.btn-save:hover { background: var(--accent-bg); filter: brightness(1.2); }
|
|
1654
|
+
.btn-confirm { flex: 1; background: var(--green); color: #0a0a0a; padding: 7px; font-size: 11px; }
|
|
1655
|
+
.btn-cancel { flex: 1; background: var(--bg-elevated); color: var(--text-muted); padding: 7px; font-size: 11px; }
|
|
1656
|
+
.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; }
|
|
1657
|
+
.save-input:focus { border-color: var(--accent); outline: none; }
|
|
696
1658
|
|
|
697
1659
|
/* 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:
|
|
1660
|
+
.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; }
|
|
1661
|
+
.variant-card:hover { border-color: var(--green); }
|
|
1662
|
+
.variant-card .vname { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
1663
|
+
.variant-card .vdesc { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
|
|
1664
|
+
.variant-card .vmeta { font-size: 10px; color: var(--text-faint); margin-top: 4px; }
|
|
1665
|
+
.variant-card .vmeta .vresult { color: var(--green); }
|
|
1666
|
+
.variant-card .vdelete { position: absolute; top: 6px; right: 8px; color: var(--red); font-size: 10px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
|
|
705
1667
|
.variant-card:hover .vdelete { opacity: 1; }
|
|
706
|
-
.variant-card .vbase { display: inline-block; font-size: 9px; padding: 1px 5px; background:
|
|
1668
|
+
.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
1669
|
|
|
708
1670
|
/* RIGHT PANEL — Simulation viewer */
|
|
709
|
-
.viewer { display: grid; grid-template-rows: auto
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
.
|
|
713
|
-
|
|
714
|
-
.
|
|
715
|
-
.
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
.
|
|
719
|
-
.
|
|
720
|
-
.
|
|
721
|
-
.
|
|
722
|
-
.
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
.
|
|
728
|
-
.
|
|
729
|
-
.
|
|
730
|
-
|
|
731
|
-
.
|
|
732
|
-
.
|
|
733
|
-
.
|
|
734
|
-
.
|
|
1671
|
+
.viewer { display: grid; grid-template-rows: auto auto auto auto auto; overflow-y: auto; height: 100%; gap: 0; }
|
|
1672
|
+
|
|
1673
|
+
/* Outcome panel */
|
|
1674
|
+
.outcome-panel { padding: 20px 24px 16px; border-bottom: 1px solid var(--border); }
|
|
1675
|
+
.outcome-statement { font-size: 16px; font-weight: 600; color: var(--text-primary); line-height: 1.4; margin-bottom: 12px; }
|
|
1676
|
+
.outcome-empty { font-size: 13px; color: var(--text-faint); font-style: italic; }
|
|
1677
|
+
.confidence-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 8px; }
|
|
1678
|
+
.confidence-card { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 8px 10px; }
|
|
1679
|
+
.confidence-card .cc-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
|
|
1680
|
+
.confidence-card .cc-value { font-size: 13px; font-weight: 600; }
|
|
1681
|
+
.confidence-card .cc-value.good { color: var(--green); }
|
|
1682
|
+
.confidence-card .cc-value.warn { color: var(--yellow); }
|
|
1683
|
+
.confidence-card .cc-value.bad { color: var(--red); }
|
|
1684
|
+
.outcome-context { font-size: 10px; color: var(--text-faint); margin-top: 6px; }
|
|
1685
|
+
|
|
1686
|
+
/* Behavior panel */
|
|
1687
|
+
.behavior-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
|
|
1688
|
+
.behavior-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
|
1689
|
+
.behavior-shifts { margin-bottom: 10px; }
|
|
1690
|
+
.behavior-shift-item { font-size: 12px; color: var(--text-secondary); padding: 4px 0 4px 12px; border-left: 2px solid var(--accent); line-height: 1.5; margin-bottom: 4px; }
|
|
1691
|
+
.behavior-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
|
|
1692
|
+
|
|
1693
|
+
.activity-toggle { font-size: 10px; color: var(--text-muted); cursor: pointer; padding: 6px 0; margin-top: 4px; }
|
|
1694
|
+
.activity-toggle:hover { color: var(--text-secondary); }
|
|
1695
|
+
.activity-timeline { display: none; max-height: 180px; overflow-y: auto; margin-top: 6px; }
|
|
1696
|
+
.activity-timeline.open { display: block; }
|
|
1697
|
+
.activity-item { font-size: 11px; color: var(--text-secondary); padding: 3px 0; border-bottom: 1px solid var(--border); }
|
|
1698
|
+
.activity-item:last-child { border-bottom: none; }
|
|
1699
|
+
.activity-agent { color: var(--blue); font-weight: 500; }
|
|
1700
|
+
|
|
1701
|
+
/* Why panel */
|
|
1702
|
+
.why-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
|
|
1703
|
+
.why-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
|
1704
|
+
.why-item { font-size: 12px; color: var(--text-secondary); padding: 4px 0 4px 12px; border-left: 2px solid var(--green); line-height: 1.5; margin-bottom: 4px; }
|
|
1705
|
+
.why-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
|
|
1706
|
+
|
|
1707
|
+
/* Export bar */
|
|
1708
|
+
.export-bar { display: flex; gap: 8px; padding: 12px 24px; border-bottom: 1px solid var(--border); background: var(--bg-secondary); }
|
|
1709
|
+
.export-btn { padding: 6px 14px; font-size: 11px; font-weight: 600; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-surface); color: var(--text-secondary); cursor: pointer; font-family: inherit; transition: all 0.2s; }
|
|
1710
|
+
.export-btn:hover { background: var(--bg-elevated); color: var(--text-primary); }
|
|
1711
|
+
.audit-btn { margin-left: auto; color: var(--text-faint); }
|
|
1712
|
+
|
|
1713
|
+
/* Audit overlay */
|
|
1714
|
+
.audit-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; background: rgba(0,0,0,0.7); }
|
|
1715
|
+
.audit-overlay.open { display: flex; align-items: center; justify-content: center; }
|
|
1716
|
+
.audit-modal { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 8px; width: 90%; max-width: 900px; max-height: 85vh; overflow-y: auto; padding: 0; }
|
|
1717
|
+
.audit-header { display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg-primary); z-index: 1; }
|
|
1718
|
+
.audit-header h2 { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; }
|
|
1719
|
+
.audit-close { margin-left: auto; padding: 4px 12px; font-size: 12px; background: none; border: 1px solid var(--border); border-radius: 4px; color: var(--text-muted); cursor: pointer; font-family: inherit; }
|
|
1720
|
+
.audit-close:hover { color: var(--text-primary); }
|
|
1721
|
+
.audit-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
|
1722
|
+
.audit-section:last-child { border-bottom: none; }
|
|
1723
|
+
.audit-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
|
1724
|
+
.audit-section .rule-card { margin-bottom: 6px; }
|
|
1725
|
+
|
|
1726
|
+
/* Print styles for PDF export */
|
|
1727
|
+
@media print {
|
|
1728
|
+
body { background: #fff !important; color: #111 !important; }
|
|
1729
|
+
.controls, .export-bar, .audit-overlay, .activity-toggle { display: none !important; }
|
|
1730
|
+
.viewer { overflow: visible !important; height: auto !important; }
|
|
1731
|
+
.outcome-panel, .behavior-panel, .why-panel { border: 1px solid #ddd !important; margin-bottom: 8px; border-radius: 6px; }
|
|
1732
|
+
.confidence-card { border: 1px solid #ddd !important; }
|
|
1733
|
+
* { color: #111 !important; background: #fff !important; border-color: #ddd !important; }
|
|
1734
|
+
.cc-value.good { color: #16a34a !important; }
|
|
1735
|
+
.cc-value.warn { color: #ca8a04 !important; }
|
|
1736
|
+
.cc-value.bad { color: #dc2626 !important; }
|
|
1737
|
+
}
|
|
1738
|
+
.center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
|
|
735
1739
|
.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:
|
|
739
|
-
|
|
740
|
-
/*
|
|
741
|
-
.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
/* Simulation Trace */
|
|
745
|
-
.trace-round { margin-bottom: 10px; border: 1px solid #1a1a1a; border-radius: 4px; overflow: hidden; }
|
|
746
|
-
.trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #111; cursor: pointer; user-select: none; }
|
|
747
|
-
.trace-round-header:hover { background: #181818; }
|
|
1740
|
+
.verdict.ALLOW { background: var(--green-bg); color: var(--green); }
|
|
1741
|
+
.verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1742
|
+
.verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
|
|
1743
|
+
|
|
1744
|
+
/* Simulation Trace (audit only) */
|
|
1745
|
+
.trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
1746
|
+
.trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
|
|
1747
|
+
.trace-round-header:hover { background: var(--bg-elevated); }
|
|
748
1748
|
.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:
|
|
1749
|
+
.trace-phase.baseline { background: var(--blue-bg); color: var(--blue); }
|
|
1750
|
+
.trace-phase.governed { background: var(--green-bg); color: var(--green); }
|
|
1751
|
+
.trace-round-label { font-size: 11px; color: var(--text-primary); font-weight: 600; }
|
|
1752
|
+
.trace-round-metrics { margin-left: auto; font-size: 10px; color: var(--text-muted); display: flex; gap: 10px; }
|
|
753
1753
|
.trace-body { padding: 0 10px 8px; }
|
|
754
1754
|
.trace-body[data-collapsed="true"] { display: none; }
|
|
755
1755
|
.trace-section { margin-top: 6px; }
|
|
756
|
-
.trace-section-label { font-size: 9px; color:
|
|
1756
|
+
.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
1757
|
.trace-section-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
|
|
758
1758
|
.trace-section-label.event::before { background: #f59e0b; }
|
|
759
1759
|
.trace-section-label.agents::before { background: #3b82f6; }
|
|
760
1760
|
.trace-section-label.governance::before { background: #10b981; }
|
|
761
|
-
.trace-event-item { font-size: 10px; color:
|
|
1761
|
+
.trace-event-item { font-size: 10px; color: var(--text-primary); padding: 3px 0 3px 11px; border-left: 2px solid #f59e0b; margin-left: 2px; }
|
|
762
1762
|
.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:
|
|
1763
|
+
.trace-event-severity.major, .trace-event-severity.extreme { background: var(--red-bg); color: var(--red); }
|
|
1764
|
+
.trace-event-severity.moderate { background: var(--yellow-bg); color: var(--yellow); }
|
|
1765
|
+
.trace-event-severity.minor { background: var(--green-bg); color: var(--green); }
|
|
766
1766
|
.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:
|
|
1767
|
+
.trace-agent-name { color: var(--blue); font-weight: 500; min-width: 80px; }
|
|
1768
|
+
.trace-agent-action { color: var(--text-secondary); flex: 1; }
|
|
769
1769
|
.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:
|
|
1770
|
+
.trace-agent-impact.positive { color: var(--green); }
|
|
1771
|
+
.trace-agent-impact.negative { color: var(--red); }
|
|
772
1772
|
.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:
|
|
1773
|
+
.trace-gov-rule { font-size: 9px; color: var(--text-muted); font-family: monospace; }
|
|
1774
|
+
.trace-gov-reason { color: var(--text-secondary); flex: 1; }
|
|
1775
|
+
.trace-dynamics { font-size: 10px; color: var(--purple); padding: 2px 0 2px 11px; border-left: 2px solid #7c3aed; margin-left: 2px; font-style: italic; }
|
|
1776
|
+
.trace-arrow { color: var(--text-faint); font-size: 10px; text-align: center; padding: 2px 0; }
|
|
1777
|
+
.trace-empty { font-size: 10px; color: var(--text-faint); font-style: italic; padding: 4px 0; }
|
|
778
1778
|
|
|
779
1779
|
/* World info */
|
|
780
|
-
.world-thesis { font-size: 11px; color:
|
|
781
|
-
.
|
|
1780
|
+
.world-thesis { font-size: 11px; color: var(--text-secondary); font-style: italic; margin-bottom: 8px; }
|
|
1781
|
+
.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; }
|
|
1782
|
+
.rule-card:hover { border-color: var(--text-muted); }
|
|
1783
|
+
.rule-card.type-invariant { border-left: 4px solid var(--green); }
|
|
1784
|
+
.rule-card.type-gate { border-left: 4px solid var(--red); }
|
|
1785
|
+
.rule-card.type-warning { border-left: 4px solid var(--yellow); }
|
|
1786
|
+
.rule-card.type-modify { border-left: 4px solid #60a5fa; }
|
|
1787
|
+
.rule-card .rule-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
1788
|
+
.rule-card .rule-icon { font-size: 12px; flex-shrink: 0; }
|
|
1789
|
+
.rule-card .rule-title { font-size: 12px; font-weight: 600; color: var(--text-primary); }
|
|
1790
|
+
.rule-card .rule-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
|
|
1791
|
+
.rule-card .rule-meta { font-size: 10px; color: var(--text-muted); margin-top: 6px; opacity: 0.8; }
|
|
1792
|
+
.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); }
|
|
1793
|
+
.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)); }
|
|
1794
|
+
.rule-card .rule-impact.visible { display: block; }
|
|
1795
|
+
.rule-card .rule-impact .impact-stat { color: var(--text-primary); font-weight: 600; }
|
|
1796
|
+
.rule-card .rule-impact .impact-label { color: var(--text-muted); }
|
|
1797
|
+
.rule-card.user-rule { border-left-color: #818cf8; background: rgba(129, 140, 248, 0.05); }
|
|
1798
|
+
.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
1799
|
|
|
783
1800
|
/* Empty state */
|
|
784
|
-
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color:
|
|
1801
|
+
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-faint); }
|
|
785
1802
|
.empty-state .icon { font-size: 48px; margin-bottom: 12px; }
|
|
786
|
-
.empty-state .msg { font-size: 13px; }
|
|
787
|
-
.empty-state .hint { font-size: 11px; color:
|
|
1803
|
+
.empty-state .msg { font-size: 13px; color: var(--text-muted); }
|
|
1804
|
+
.empty-state .hint { font-size: 11px; color: var(--text-faint); margin-top: 6px; }
|
|
788
1805
|
|
|
789
1806
|
/* System Shift Card */
|
|
790
|
-
.system-shift { display: none; margin: 12px 16px; border: 1px solid
|
|
1807
|
+
.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
1808
|
.system-shift.visible { display: block; }
|
|
792
1809
|
@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:
|
|
1810
|
+
.ss-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background: var(--green-bg); border-bottom: 1px solid var(--green-bg); }
|
|
1811
|
+
.ss-icon { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgba(74,222,128,0.4); }
|
|
1812
|
+
.ss-title { font-size: 11px; font-weight: 700; color: var(--green); text-transform: uppercase; letter-spacing: 1.5px; }
|
|
1813
|
+
.ss-rule { font-size: 13px; font-weight: 600; color: var(--text-primary); padding: 10px 14px 0; }
|
|
797
1814
|
.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:
|
|
1815
|
+
.ss-section { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
|
|
1816
|
+
.ss-section-label { font-size: 9px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
|
|
1817
|
+
.ss-adapt-rate { font-size: 20px; font-weight: 700; color: var(--green); }
|
|
1818
|
+
.ss-adapt-desc { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
|
|
1819
|
+
.ss-shift-item { font-size: 11px; color: var(--text-secondary); padding: 2px 0; display: flex; align-items: center; gap: 6px; }
|
|
1820
|
+
.ss-shift-arrow { color: var(--green); font-weight: 600; }
|
|
1821
|
+
.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; }
|
|
1822
|
+
.ss-impact-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--text-secondary); padding: 2px 0; }
|
|
1823
|
+
.ss-impact-delta { color: var(--green); font-weight: 600; }
|
|
1824
|
+
.ss-impact-delta.negative { color: var(--red); }
|
|
1825
|
+
.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; }
|
|
1826
|
+
.ss-scale { font-size: 10px; color: var(--text-muted); padding: 0 14px 6px; }
|
|
1827
|
+
.ss-scale strong { color: var(--text-secondary); }
|
|
1828
|
+
.ss-flow { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 10px; color: var(--text-faint); }
|
|
1829
|
+
.ss-flow-arrow { color: var(--green); }
|
|
1830
|
+
.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; }
|
|
1831
|
+
.ss-raw-toggle:hover { color: var(--text-secondary); }
|
|
815
1832
|
.ss-raw-toggle .arrow { transition: transform 0.2s; }
|
|
816
1833
|
.ss-raw-toggle.open .arrow { transform: rotate(90deg); }
|
|
817
1834
|
.ss-raw-detail { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
|
|
818
1835
|
.ss-raw-detail.open { max-height: 200px; overflow-y: auto; }
|
|
819
1836
|
.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:
|
|
1837
|
+
.ss-raw-item { font-size: 10px; color: var(--text-muted); padding: 2px 0; display: flex; gap: 6px; }
|
|
1838
|
+
.ss-raw-item .raw-agent { color: var(--text-secondary); min-width: 100px; }
|
|
1839
|
+
.ss-raw-item .raw-action { color: var(--text-secondary); flex: 1; }
|
|
823
1840
|
.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:
|
|
1841
|
+
.ss-raw-item .raw-verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1842
|
+
.ss-raw-item .raw-verdict.MODIFY { background: var(--yellow-bg); color: var(--yellow); }
|
|
826
1843
|
|
|
827
1844
|
/* 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:
|
|
1845
|
+
.integrate-section { background: var(--accent-bg); border: 1px solid var(--accent-bg); border-radius: 6px; padding: 10px 12px; margin-top: 8px; }
|
|
1846
|
+
.integrate-section h4 { font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
|
1847
|
+
.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; }
|
|
1848
|
+
.integrate-code .kw { color: var(--accent); }
|
|
1849
|
+
.integrate-code .str { color: var(--green); }
|
|
1850
|
+
.integrate-code .comment { color: var(--text-muted); }
|
|
1851
|
+
.integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
|
|
1852
|
+
.integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
|
|
1853
|
+
|
|
1854
|
+
/* Language tabs for integration code */
|
|
1855
|
+
.lang-tabs { display: flex; gap: 2px; margin-bottom: 6px; }
|
|
1856
|
+
.lang-tab { padding: 4px 10px; font-size: 10px; font-weight: 600; color: var(--text-muted); background: transparent; border: 1px solid transparent; border-bottom: none; border-radius: 4px 4px 0 0; cursor: pointer; font-family: inherit; transition: all 0.2s; }
|
|
1857
|
+
.lang-tab:hover { color: var(--text-secondary); }
|
|
1858
|
+
.lang-tab.active { color: var(--accent); background: var(--bg-surface); border-color: var(--bg-elevated); }
|
|
1859
|
+
.lang-code-panel { display: none; }
|
|
1860
|
+
.lang-code-panel.active { display: block; }
|
|
1861
|
+
.integrate-hint { font-size: 10px; color: var(--text-muted); margin-top: 6px; line-height: 1.5; }
|
|
1862
|
+
.integrate-hint strong { color: var(--text-secondary); }
|
|
1863
|
+
.integrate-works { font-size: 10px; color: var(--text-faint); margin-top: 8px; line-height: 1.6; }
|
|
1864
|
+
.integrate-works strong { color: var(--text-muted); }
|
|
1865
|
+
|
|
1866
|
+
/* Framework selector tabs */
|
|
1867
|
+
.fw-tabs { display: flex; gap: 4px; margin-bottom: 10px; }
|
|
1868
|
+
.fw-tab { flex: 1; padding: 8px 6px; font-size: 10px; font-weight: 700; color: var(--text-muted); background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-family: inherit; transition: all 0.2s; text-align: center; line-height: 1.3; }
|
|
1869
|
+
.fw-tab:hover { border-color: var(--text-muted); color: var(--text-secondary); }
|
|
1870
|
+
.fw-tab.active { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
|
|
1871
|
+
.fw-tab .fw-sub { display: block; font-size: 8px; font-weight: 400; color: var(--text-faint); margin-top: 2px; }
|
|
1872
|
+
.fw-tab.active .fw-sub { color: var(--accent); opacity: 0.7; }
|
|
1873
|
+
.fw-panel { display: none; }
|
|
1874
|
+
.fw-panel.active { display: block; }
|
|
1875
|
+
|
|
1876
|
+
/* Architecture callout */
|
|
1877
|
+
.arch-callout { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; }
|
|
1878
|
+
.arch-callout .arch-title { font-size: 10px; font-weight: 700; color: var(--text-secondary); margin-bottom: 4px; }
|
|
1879
|
+
.arch-flow { font-size: 10px; color: var(--text-muted); font-family: monospace; line-height: 1.6; }
|
|
1880
|
+
.arch-flow .arch-yes { color: var(--green); }
|
|
1881
|
+
.arch-flow .arch-no { color: var(--red); }
|
|
1882
|
+
.arch-warn { font-size: 9px; color: var(--red); margin-top: 6px; padding: 4px 8px; background: #2d0606; border-radius: 4px; line-height: 1.5; }
|
|
1883
|
+
|
|
1884
|
+
/* Responsive / Adaptive Panels */
|
|
1885
|
+
@media (max-width: 1200px) {
|
|
1886
|
+
.layout { grid-template-columns: 300px 1fr; }
|
|
1887
|
+
.controls { padding: 12px; }
|
|
1888
|
+
}
|
|
1889
|
+
@media (max-width: 960px) {
|
|
1890
|
+
.layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; height: auto; min-height: 100vh; }
|
|
1891
|
+
.controls { border-right: none; border-bottom: 1px solid var(--border); max-height: 50vh; overflow-y: auto; }
|
|
1892
|
+
.viewer { min-height: 60vh; }
|
|
1893
|
+
.viewer-top { grid-template-columns: 1fr; }
|
|
1894
|
+
.viewer-mid { grid-template-columns: 1fr; }
|
|
1895
|
+
.metric-grid { grid-template-columns: repeat(2, 1fr); }
|
|
1896
|
+
}
|
|
1897
|
+
@media (max-width: 640px) {
|
|
1898
|
+
.header h1 { font-size: 14px; }
|
|
1899
|
+
.header .sub { font-size: 10px; }
|
|
1900
|
+
.controls { padding: 10px; }
|
|
1901
|
+
.ctrl-section h3 { font-size: 10px; }
|
|
1902
|
+
.confidence-grid { grid-template-columns: 1fr; }
|
|
1903
|
+
.agent-name { width: 80px; }
|
|
1904
|
+
.integrate-code { font-size: 9px; padding: 6px; }
|
|
1905
|
+
.scenario-btn { padding: 6px; }
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/* Rule editor */
|
|
1909
|
+
.rule-editor { margin-top: 8px; }
|
|
1910
|
+
.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; }
|
|
1911
|
+
.rule-input:focus { border-color: var(--accent); outline: none; }
|
|
1912
|
+
.rule-input::placeholder { color: var(--text-faint); }
|
|
1913
|
+
.btn-parse { background: var(--accent-bg); color: var(--accent); margin-top: 6px; padding: 8px; font-size: 11px; }
|
|
1914
|
+
.btn-parse:hover { filter: brightness(1.2); }
|
|
1915
|
+
.btn-parse:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1916
|
+
.parsed-rules { margin-top: 8px; }
|
|
1917
|
+
.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; }
|
|
1918
|
+
.parsed-rule:hover { border-color: var(--text-muted); }
|
|
1919
|
+
.parsed-rule.enforcement-block { border-left: 4px solid var(--red); }
|
|
1920
|
+
.parsed-rule.enforcement-allow { border-left: 4px solid var(--green); }
|
|
1921
|
+
.parsed-rule.enforcement-modify { border-left: 4px solid #60a5fa; }
|
|
1922
|
+
.parsed-rule.enforcement-warn { border-left: 4px solid var(--yellow); }
|
|
1923
|
+
.parsed-rule .pr-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
1924
|
+
.parsed-rule .pr-icon { font-size: 12px; }
|
|
1925
|
+
.parsed-rule .pr-action { font-weight: 700; font-size: 12px; color: var(--text-primary); text-transform: capitalize; }
|
|
1926
|
+
.parsed-rule .pr-desc { color: var(--text-secondary); font-size: 11px; line-height: 1.4; }
|
|
1927
|
+
.parsed-rule .pr-patterns { color: var(--text-muted); font-size: 10px; margin-top: 6px; opacity: 0.8; }
|
|
1928
|
+
.btn-apply-rules { background: var(--green); color: #0a0a0a; margin-top: 6px; padding: 8px; font-size: 11px; }
|
|
1929
|
+
.btn-apply-rules:hover { filter: brightness(0.9); }
|
|
1930
|
+
.rule-status { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
|
|
1931
|
+
.rule-status.success { color: var(--green); }
|
|
1932
|
+
.rule-status.error { color: var(--red); }
|
|
1933
|
+
.rule-examples { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.6; }
|
|
1934
|
+
.rule-examples code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
|
|
1935
|
+
|
|
1936
|
+
/* World Action Bar */
|
|
1937
|
+
.world-action-bar { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
1938
|
+
.btn-world-action { flex: 1; min-width: 0; padding: 6px 4px; font-size: 10px; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font-family: inherit; text-align: center; transition: all 0.2s; white-space: nowrap; }
|
|
1939
|
+
.btn-world-action:hover { border-color: var(--accent); color: var(--accent); }
|
|
1940
|
+
.btn-world-action.btn-export { color: var(--green); border-color: var(--green); opacity: 0.7; }
|
|
1941
|
+
.btn-world-action.btn-export:hover { opacity: 1; }
|
|
1942
|
+
|
|
1943
|
+
/* World Source Tabs */
|
|
1944
|
+
.world-source-tabs { display: flex; gap: 4px; }
|
|
1945
|
+
.ws-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 10px 6px; background: var(--bg-surface); border: 2px solid var(--border); border-radius: 6px; cursor: pointer; transition: all 0.2s; text-align: center; }
|
|
1946
|
+
.ws-tab:hover { border-color: var(--text-muted); }
|
|
1947
|
+
.ws-tab.active { border-color: var(--accent); background: var(--accent-bg); }
|
|
1948
|
+
.ws-tab input[type="radio"] { display: none; }
|
|
1949
|
+
.ws-label { font-size: 11px; font-weight: 700; color: var(--text-primary); }
|
|
1950
|
+
.ws-hint { font-size: 9px; color: var(--text-muted); margin-top: 2px; }
|
|
1951
|
+
.ws-tab.active .ws-label { color: var(--accent); }
|
|
1952
|
+
|
|
1953
|
+
/* World Source Panels */
|
|
1954
|
+
.world-source-panel { animation: fadeIn 0.2s ease; }
|
|
1955
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
1956
|
+
|
|
1957
|
+
/* Custom World Header */
|
|
1958
|
+
.custom-world-header { margin-bottom: 12px; }
|
|
1959
|
+
.world-name-input { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
|
|
1960
|
+
.world-name-input:focus { border-color: var(--accent); outline: none; }
|
|
1961
|
+
.world-thesis-input { width: 100%; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 11px; resize: none; height: 36px; }
|
|
1962
|
+
.world-thesis-input:focus { border-color: var(--accent); outline: none; }
|
|
1963
|
+
|
|
1964
|
+
/* Rule editor enhancements */
|
|
1965
|
+
.rule-editor-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600; }
|
|
1966
|
+
.rule-input-large { min-height: 120px; }
|
|
1967
|
+
.btn-generate-world { background: var(--accent); color: #fff; margin-top: 8px; padding: 10px; font-size: 12px; font-weight: 700; width: 100%; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
|
|
1968
|
+
.btn-generate-world:hover { filter: brightness(1.1); }
|
|
1969
|
+
|
|
1970
|
+
/* Upload Zone */
|
|
1971
|
+
.upload-zone { border: 2px dashed var(--border-subtle); border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.2s; margin-bottom: 12px; }
|
|
1972
|
+
.upload-zone:hover, .upload-zone.dragover { border-color: var(--accent); background: var(--accent-bg); }
|
|
1973
|
+
.upload-icon { font-size: 28px; margin-bottom: 8px; }
|
|
1974
|
+
.upload-label { font-size: 12px; color: var(--text-secondary); }
|
|
1975
|
+
.upload-or { font-size: 10px; color: var(--text-faint); margin: 8px 0; }
|
|
1976
|
+
.btn-upload-browse { background: var(--bg-elevated); color: var(--text-primary); border: 1px solid var(--border); padding: 6px 16px; border-radius: 4px; cursor: pointer; font-family: inherit; font-size: 11px; }
|
|
1977
|
+
.upload-paste-section { margin-bottom: 12px; }
|
|
1978
|
+
.btn-load-world { background: var(--green); color: #0a0a0a; width: 100%; padding: 10px; font-size: 12px; font-weight: 700; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
|
|
1979
|
+
.btn-load-world:hover { filter: brightness(0.9); }
|
|
1980
|
+
|
|
1981
|
+
/* Loaded World Card */
|
|
1982
|
+
.loaded-world-card { background: var(--green-bg); border: 1px solid var(--green); border-radius: 6px; padding: 12px; margin-top: 10px; }
|
|
1983
|
+
.lw-name { font-size: 13px; font-weight: 700; color: var(--green); }
|
|
1984
|
+
.lw-thesis { font-size: 11px; color: var(--text-secondary); margin-top: 4px; font-style: italic; }
|
|
1985
|
+
.lw-stats { font-size: 10px; color: var(--text-muted); margin-top: 6px; }
|
|
1986
|
+
|
|
1987
|
+
/* Schema Reference */
|
|
1988
|
+
.schema-ref { font-size: 10px; color: var(--text-muted); }
|
|
1989
|
+
.schema-item { padding: 3px 0; border-bottom: 1px solid var(--border); }
|
|
1990
|
+
.schema-item:last-child { border-bottom: none; }
|
|
1991
|
+
.schema-item code { color: var(--accent); background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; }
|
|
836
1992
|
</style>
|
|
837
1993
|
</head>
|
|
838
1994
|
<body>
|
|
839
1995
|
<div class="header">
|
|
840
1996
|
<div style="display:flex;align-items:center">
|
|
841
1997
|
<h1>NV-SIM</h1>
|
|
842
|
-
<span class="sub">
|
|
1998
|
+
<span class="sub">Governance Runtime</span>
|
|
1999
|
+
</div>
|
|
2000
|
+
<div class="header-right">
|
|
2001
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">Light Mode</button>
|
|
2002
|
+
<span id="status" class="status idle">Ready</span>
|
|
843
2003
|
</div>
|
|
844
|
-
<span id="status" class="status idle">Ready</span>
|
|
845
2004
|
</div>
|
|
846
2005
|
|
|
847
2006
|
<div class="layout">
|
|
848
2007
|
<!-- LEFT: CONTROLS -->
|
|
849
2008
|
<div class="controls" id="controls-panel">
|
|
850
|
-
<!--
|
|
2009
|
+
<!-- World Action Bar -->
|
|
2010
|
+
<div class="world-action-bar">
|
|
2011
|
+
<button class="btn btn-world-action" id="new-world-btn" title="Clear everything and start fresh">+ New World</button>
|
|
2012
|
+
<button class="btn btn-world-action" id="load-file-btn" title="Load a .json world file">Load World File</button>
|
|
2013
|
+
<button class="btn btn-world-action" id="clear-rules-btn" title="Clear custom rules only">Clear Rules</button>
|
|
2014
|
+
<button class="btn btn-world-action btn-export" id="export-world-btn" title="Export current world as JSON">Save as World File</button>
|
|
2015
|
+
</div>
|
|
2016
|
+
|
|
2017
|
+
<!-- World Source selector -->
|
|
851
2018
|
<div class="ctrl-section">
|
|
852
|
-
<h3>
|
|
853
|
-
<div class="
|
|
854
|
-
<
|
|
855
|
-
<
|
|
856
|
-
|
|
2019
|
+
<h3>World Source</h3>
|
|
2020
|
+
<div class="world-source-tabs">
|
|
2021
|
+
<label class="ws-tab active" data-source="preset">
|
|
2022
|
+
<input type="radio" name="world-source" value="preset" checked>
|
|
2023
|
+
<span class="ws-label">Preset</span>
|
|
2024
|
+
<span class="ws-hint">Demo scenarios</span>
|
|
2025
|
+
</label>
|
|
2026
|
+
<label class="ws-tab" data-source="custom">
|
|
2027
|
+
<input type="radio" name="world-source" value="custom">
|
|
2028
|
+
<span class="ws-label">Custom Rules</span>
|
|
2029
|
+
<span class="ws-hint">Define your world</span>
|
|
2030
|
+
</label>
|
|
2031
|
+
<label class="ws-tab" data-source="upload">
|
|
2032
|
+
<input type="radio" name="world-source" value="upload">
|
|
2033
|
+
<span class="ws-label">World File</span>
|
|
2034
|
+
<span class="ws-hint">JSON / .nv-world</span>
|
|
2035
|
+
</label>
|
|
857
2036
|
</div>
|
|
858
|
-
<div id="engine-status" style="font-size:10px;color:#555;margin-top:4px"></div>
|
|
859
2037
|
</div>
|
|
860
2038
|
|
|
861
|
-
<!--
|
|
862
|
-
<div class="
|
|
863
|
-
<
|
|
864
|
-
|
|
865
|
-
<
|
|
2039
|
+
<!-- SOURCE: Preset -->
|
|
2040
|
+
<div class="world-source-panel" id="source-preset">
|
|
2041
|
+
<div class="ctrl-section">
|
|
2042
|
+
<h3>World</h3>
|
|
2043
|
+
<div class="ctrl-row">
|
|
2044
|
+
<select id="world-select"></select>
|
|
2045
|
+
</div>
|
|
2046
|
+
<div id="world-thesis" class="world-thesis"></div>
|
|
2047
|
+
</div>
|
|
2048
|
+
|
|
2049
|
+
<!-- State variables (dynamic sliders) -->
|
|
2050
|
+
<div class="ctrl-section" id="state-vars-section" style="display:none">
|
|
2051
|
+
<h3>World Rules</h3>
|
|
2052
|
+
<div id="state-vars"></div>
|
|
2053
|
+
</div>
|
|
2054
|
+
|
|
2055
|
+
<!-- Scenario presets (collapsed by default) -->
|
|
2056
|
+
<div class="ctrl-section">
|
|
2057
|
+
<h3 style="cursor:pointer" onclick="var el=document.getElementById('scenario-list');var a=this.querySelector('.arrow');if(el.style.display==='none'){el.style.display='';a.textContent='▼'}else{el.style.display='none';a.textContent='▶'}"><span class="arrow" style="font-size:9px;margin-right:4px">▶</span>Scenarios</h3>
|
|
2058
|
+
<div id="scenario-list" style="display:none"></div>
|
|
866
2059
|
</div>
|
|
867
|
-
<div id="world-thesis" class="world-thesis"></div>
|
|
868
2060
|
</div>
|
|
869
2061
|
|
|
870
|
-
<!--
|
|
871
|
-
<div class="
|
|
872
|
-
<
|
|
873
|
-
|
|
2062
|
+
<!-- SOURCE: Custom Rules (Define Your World) -->
|
|
2063
|
+
<div class="world-source-panel" id="source-custom" style="display:none">
|
|
2064
|
+
<div class="ctrl-section">
|
|
2065
|
+
<h3>Define Your World</h3>
|
|
2066
|
+
<div class="custom-world-header">
|
|
2067
|
+
<input type="text" class="world-name-input" id="custom-world-name" placeholder="World name (e.g. Marketing Governance)">
|
|
2068
|
+
<textarea class="world-thesis-input" id="custom-world-thesis" placeholder="What is this world about? (thesis)"></textarea>
|
|
2069
|
+
</div>
|
|
2070
|
+
<div class="rule-editor">
|
|
2071
|
+
<div class="rule-editor-label">Type your governance rules:</div>
|
|
2072
|
+
<textarea class="rule-input rule-input-large" id="rule-input" placeholder="No agent may spend more than $10k without approval All outbound emails must be reviewed Block deletion of production data Limit API calls to 100 per minute Require manager approval for refunds over $500"></textarea>
|
|
2073
|
+
<button class="btn btn-generate-world" id="parse-rules-btn">Generate World</button>
|
|
2074
|
+
<div id="parsed-rules" class="parsed-rules"></div>
|
|
2075
|
+
<div id="rule-status" class="rule-status"></div>
|
|
2076
|
+
<div class="rule-examples">
|
|
2077
|
+
Rule patterns:<br>
|
|
2078
|
+
<code>Block [action]</code> — hard suppression<br>
|
|
2079
|
+
<code>Limit [X] to [N]</code> — cap extremes<br>
|
|
2080
|
+
<code>Require [X] for [Y]</code> — structural constraint<br>
|
|
2081
|
+
<code>Pause [X] for review</code> — human-in-the-loop<br>
|
|
2082
|
+
<code>Allow [X]</code> — explicit permission<br>
|
|
2083
|
+
<code>Monitor [X]</code> — circuit breaker gate
|
|
2084
|
+
</div>
|
|
2085
|
+
</div>
|
|
2086
|
+
</div>
|
|
2087
|
+
|
|
2088
|
+
<!-- Base world (optional) -->
|
|
2089
|
+
<div class="ctrl-section">
|
|
2090
|
+
<h3>Base World (Optional)</h3>
|
|
2091
|
+
<div class="ctrl-row">
|
|
2092
|
+
<select id="custom-base-world">
|
|
2093
|
+
<option value="">None — start from scratch</option>
|
|
2094
|
+
</select>
|
|
2095
|
+
<div style="font-size:10px;color:var(--text-faint);margin-top:4px">Layer your rules on top of a preset world</div>
|
|
2096
|
+
</div>
|
|
2097
|
+
</div>
|
|
874
2098
|
</div>
|
|
875
2099
|
|
|
876
|
-
<!--
|
|
877
|
-
<div class="
|
|
878
|
-
<
|
|
879
|
-
|
|
2100
|
+
<!-- SOURCE: Upload World File -->
|
|
2101
|
+
<div class="world-source-panel" id="source-upload" style="display:none">
|
|
2102
|
+
<div class="ctrl-section">
|
|
2103
|
+
<h3>Load World File</h3>
|
|
2104
|
+
<div class="upload-zone" id="upload-zone">
|
|
2105
|
+
<div class="upload-icon">📄</div>
|
|
2106
|
+
<div class="upload-label">Drop a .json or .nv-world file here</div>
|
|
2107
|
+
<div class="upload-or">or</div>
|
|
2108
|
+
<button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
|
|
2109
|
+
<input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
|
|
2110
|
+
</div>
|
|
2111
|
+
<div class="upload-paste-section">
|
|
2112
|
+
<div class="rule-editor-label">Or paste world JSON:</div>
|
|
2113
|
+
<textarea class="rule-input rule-input-large" id="world-json-input" placeholder='{ "name": "Marketing Governance", "thesis": "All marketing actions are governed", "invariants": [...], "rules": [...], "gates": [...] }'></textarea>
|
|
2114
|
+
</div>
|
|
2115
|
+
<button class="btn btn-load-world" id="load-world-btn">Load into Runtime</button>
|
|
2116
|
+
<div id="upload-status" class="rule-status"></div>
|
|
2117
|
+
|
|
2118
|
+
<!-- Loaded world info -->
|
|
2119
|
+
<div id="loaded-world-info" style="display:none">
|
|
2120
|
+
<div class="loaded-world-card">
|
|
2121
|
+
<div class="lw-name" id="lw-name"></div>
|
|
2122
|
+
<div class="lw-thesis" id="lw-thesis"></div>
|
|
2123
|
+
<div class="lw-stats" id="lw-stats"></div>
|
|
2124
|
+
</div>
|
|
2125
|
+
</div>
|
|
2126
|
+
</div>
|
|
2127
|
+
|
|
2128
|
+
<!-- World file schema reference -->
|
|
2129
|
+
<div class="ctrl-section">
|
|
2130
|
+
<h3>World File Schema</h3>
|
|
2131
|
+
<div class="schema-ref">
|
|
2132
|
+
<div class="schema-item"><code>name</code> — world name</div>
|
|
2133
|
+
<div class="schema-item"><code>thesis</code> — what this world is about</div>
|
|
2134
|
+
<div class="schema-item"><code>rules[]</code> — governance rules (plain English or structured)</div>
|
|
2135
|
+
<div class="schema-item"><code>invariants[]</code> — rules that always hold <code>{id, description}</code></div>
|
|
2136
|
+
<div class="schema-item"><code>gates[]</code> — viability thresholds <code>{id, label, condition, severity}</code></div>
|
|
2137
|
+
<div class="schema-item"><code>state_variables[]</code> — sliders <code>{id, label, type, range, default_value}</code></div>
|
|
2138
|
+
</div>
|
|
2139
|
+
</div>
|
|
2140
|
+
</div>
|
|
2141
|
+
|
|
2142
|
+
<!-- Simulation Engine (demoted, below world source) -->
|
|
2143
|
+
<div class="ctrl-section" style="margin-top:8px">
|
|
2144
|
+
<h3>Engine</h3>
|
|
2145
|
+
<div class="ctrl-row">
|
|
2146
|
+
<select id="engine-select">
|
|
2147
|
+
<option value="nv-sim" selected>NV-SIM (Built-in)</option>
|
|
2148
|
+
</select>
|
|
2149
|
+
</div>
|
|
2150
|
+
<div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
|
|
880
2151
|
</div>
|
|
881
2152
|
|
|
882
2153
|
<!-- Narrative injection -->
|
|
@@ -920,122 +2191,532 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
920
2191
|
|
|
921
2192
|
<!-- Saved variants -->
|
|
922
2193
|
<div class="ctrl-section" style="margin-top:16px">
|
|
923
|
-
<h3>
|
|
2194
|
+
<h3>Your Worlds</h3>
|
|
924
2195
|
<div id="variant-list"><div style="font-size:11px;color:#333">No saved variants yet</div></div>
|
|
925
2196
|
</div>
|
|
926
2197
|
|
|
927
2198
|
<!-- Integration Quick-Start -->
|
|
928
2199
|
<div class="ctrl-section" style="margin-top:16px">
|
|
929
|
-
<h3>
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
<div class="
|
|
2200
|
+
<h3>Govern Any Agent System</h3>
|
|
2201
|
+
|
|
2202
|
+
<div class="arch-callout">
|
|
2203
|
+
<div class="arch-title">Pre-Execution Enforcement</div>
|
|
2204
|
+
<div class="arch-flow">
|
|
2205
|
+
Agent decides → <span class="arch-yes">Governance evaluates</span> → <span class="arch-yes">THEN executes</span>
|
|
2206
|
+
</div>
|
|
2207
|
+
<div class="arch-warn">Governance MUST happen BEFORE execution. Post-execution = audit only, not control.</div>
|
|
2208
|
+
</div>
|
|
2209
|
+
|
|
2210
|
+
<div class="fw-tabs">
|
|
2211
|
+
<button class="fw-tab active" onclick="switchFw('generic',this)">Generic<span class="fw-sub">any agent</span></button>
|
|
2212
|
+
<button class="fw-tab" onclick="switchFw('scienceclaw',this)">ScienceClaw<span class="fw-sub">research</span></button>
|
|
2213
|
+
<button class="fw-tab" onclick="switchFw('mirofish',this)">MiroFish<span class="fw-sub">OASIS</span></button>
|
|
2214
|
+
<button class="fw-tab" onclick="switchFw('langchain',this)">LangChain<span class="fw-sub">tools</span></button>
|
|
2215
|
+
</div>
|
|
2216
|
+
|
|
2217
|
+
<div class="fw-panel active" id="fw-generic">
|
|
2218
|
+
<div class="integrate-section">
|
|
2219
|
+
<h4>Insert before your agent executes</h4>
|
|
2220
|
+
<div class="lang-tabs">
|
|
2221
|
+
<button class="lang-tab active" onclick="switchLang('generic-py',this)">Python</button>
|
|
2222
|
+
<button class="lang-tab" onclick="switchLang('generic-js',this)">JavaScript</button>
|
|
2223
|
+
<button class="lang-tab" onclick="switchLang('generic-curl',this)">cURL</button>
|
|
2224
|
+
</div>
|
|
2225
|
+
<div class="lang-code-panel active" id="lang-generic-py"><div class="integrate-code"><span class="kw">import</span> requests
|
|
2226
|
+
|
|
2227
|
+
<span class="kw">for</span> agent <span class="kw">in</span> agents:
|
|
2228
|
+
raw_action = agent.decide()
|
|
2229
|
+
|
|
2230
|
+
<span class="kw">try</span>:
|
|
2231
|
+
verdict = requests.post(
|
|
2232
|
+
<span class="str">"http://localhost:3456/api/evaluate"</span>,
|
|
2233
|
+
json={<span class="str">"actor"</span>: agent.id,
|
|
2234
|
+
<span class="str">"action"</span>: raw_action,
|
|
2235
|
+
<span class="str">"world"</span>: <span class="str">"trading"</span>},
|
|
2236
|
+
timeout=0.5
|
|
2237
|
+
).json()
|
|
2238
|
+
<span class="kw">except</span>:
|
|
2239
|
+
verdict = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
|
|
2240
|
+
|
|
2241
|
+
<span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
|
|
2242
|
+
<span class="kw">continue</span> <span class="comment"># skip this action entirely</span>
|
|
2243
|
+
<span class="kw">elif</span> verdict[<span class="str">"decision"</span>] == <span class="str">"MODIFY"</span>:
|
|
2244
|
+
raw_action = verdict[<span class="str">"modified_action"</span>]
|
|
2245
|
+
|
|
2246
|
+
environment.apply(agent, raw_action)</div></div>
|
|
2247
|
+
<div class="lang-code-panel" id="lang-generic-js"><div class="integrate-code"><span class="kw">for</span> (<span class="kw">const</span> agent <span class="kw">of</span> agents) {
|
|
2248
|
+
<span class="kw">let</span> action = agent.decide()
|
|
2249
|
+
|
|
2250
|
+
<span class="kw">const</span> verdict = <span class="kw">await</span> fetch(
|
|
2251
|
+
<span class="str">"http://localhost:3456/api/evaluate"</span>, {
|
|
2252
|
+
method: <span class="str">"POST"</span>,
|
|
2253
|
+
headers: {<span class="str">"Content-Type"</span>: <span class="str">"application/json"</span>},
|
|
2254
|
+
body: JSON.stringify({
|
|
2255
|
+
actor: agent.id, action, world: <span class="str">"trading"</span>
|
|
2256
|
+
})
|
|
2257
|
+
}).then(r => r.json())
|
|
933
2258
|
|
|
2259
|
+
<span class="kw">if</span> (verdict.decision === <span class="str">"BLOCK"</span>) <span class="kw">continue</span>
|
|
2260
|
+
<span class="kw">if</span> (verdict.decision === <span class="str">"MODIFY"</span>)
|
|
2261
|
+
action = verdict.modified_action
|
|
2262
|
+
|
|
2263
|
+
environment.apply(agent, action)
|
|
2264
|
+
}</div></div>
|
|
2265
|
+
<div class="lang-code-panel" id="lang-generic-curl"><div class="integrate-code">curl -X POST http://localhost:3456/api/evaluate \
|
|
2266
|
+
-H <span class="str">"Content-Type: application/json"</span> \
|
|
2267
|
+
-d <span class="str">'{"actor":"agent_1","action":"panic_sell"}'</span>
|
|
2268
|
+
|
|
2269
|
+
<span class="comment"># Returns: {"decision":"BLOCK","reason":"..."}</span>
|
|
2270
|
+
<span class="comment"># Watch it appear in the dashboard →</span></div></div>
|
|
2271
|
+
</div>
|
|
2272
|
+
</div>
|
|
2273
|
+
|
|
2274
|
+
<div class="fw-panel" id="fw-scienceclaw">
|
|
2275
|
+
<div class="integrate-section">
|
|
2276
|
+
<h4>Detect action type, then evaluate</h4>
|
|
2277
|
+
<div style="font-size:10px;color:var(--yellow);margin-bottom:6px;line-height:1.4">
|
|
2278
|
+
Don't hardcode <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">action = "post"</code> — detect intent from agent output so rules can differentiate.
|
|
2279
|
+
</div>
|
|
2280
|
+
<div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate, detect_action_type
|
|
2281
|
+
|
|
2282
|
+
<span class="comment"># Step 1: Agent generates content</span>
|
|
2283
|
+
results = generator.run_pubmed_search(topic)
|
|
2284
|
+
output = generator.generate_content(topic, results)
|
|
2285
|
+
|
|
2286
|
+
<span class="comment"># Step 2: Detect what kind of action this is</span>
|
|
2287
|
+
action = detect_action_type(output[<span class="str">"content"</span>])
|
|
2288
|
+
<span class="comment"># → "analyze", "publish", "cite", "recommend"</span>
|
|
2289
|
+
|
|
2290
|
+
<span class="comment"># Step 3: Evaluate BEFORE executing</span>
|
|
934
2291
|
verdict = evaluate(
|
|
935
|
-
actor=<span class="str">"
|
|
936
|
-
action
|
|
937
|
-
world=<span class="str">"
|
|
2292
|
+
actor=<span class="str">"Harry"</span>,
|
|
2293
|
+
action=action,
|
|
2294
|
+
world=<span class="str">"research"</span>
|
|
938
2295
|
)
|
|
939
2296
|
|
|
940
|
-
<span class="
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
2297
|
+
<span class="comment"># Step 4: Only execute if allowed</span>
|
|
2298
|
+
<span class="kw">if</span> verdict[<span class="str">"decision"</span>] != <span class="str">"BLOCK"</span>:
|
|
2299
|
+
generator.post_to_infinite(output)</div>
|
|
2300
|
+
<div style="font-size:9px;color:var(--text-muted);margin-top:6px;line-height:1.5">
|
|
2301
|
+
See <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">bridge/scienceclaw_governed.py</code> for full example
|
|
2302
|
+
</div>
|
|
2303
|
+
</div>
|
|
2304
|
+
</div>
|
|
2305
|
+
|
|
2306
|
+
<div class="fw-panel" id="fw-mirofish">
|
|
2307
|
+
<div class="integrate-section">
|
|
2308
|
+
<h4>Replace the dict comprehension</h4>
|
|
2309
|
+
<div style="font-size:10px;color:var(--red);margin-bottom:6px;line-height:1.4">
|
|
2310
|
+
MiroFish executes inside <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">env.step()</code> — governance MUST happen before that call.
|
|
2311
|
+
</div>
|
|
2312
|
+
<div class="integrate-code" style="margin-bottom:6px;border-left:3px solid var(--red);opacity:0.6"><span class="comment"># ❌ BEFORE (opaque — no governance possible)</span>
|
|
2313
|
+
actions = {agent: LLMAction()
|
|
2314
|
+
<span class="kw">for</span> _, agent <span class="kw">in</span> active_agents}
|
|
2315
|
+
<span class="kw">await</span> result.env.step(actions)</div>
|
|
2316
|
+
<div class="integrate-code" style="border-left:3px solid var(--green)"><span class="comment"># ✅ AFTER (expand the loop, insert governance)</span>
|
|
2317
|
+
<span class="kw">import</span> requests
|
|
2318
|
+
|
|
2319
|
+
actions = {}
|
|
2320
|
+
<span class="kw">for</span> _, agent <span class="kw">in</span> active_agents:
|
|
2321
|
+
raw_action = LLMAction()
|
|
2322
|
+
|
|
2323
|
+
<span class="kw">try</span>:
|
|
2324
|
+
verdict = requests.post(
|
|
2325
|
+
<span class="str">"http://localhost:3456/api/evaluate"</span>,
|
|
2326
|
+
json={
|
|
2327
|
+
<span class="str">"actor"</span>: getattr(agent, <span class="str">"id"</span>, str(agent)),
|
|
2328
|
+
<span class="str">"action"</span>: str(raw_action),
|
|
2329
|
+
<span class="str">"world"</span>: <span class="str">"social_media"</span>
|
|
2330
|
+
},
|
|
2331
|
+
timeout=0.5
|
|
2332
|
+
).json()
|
|
2333
|
+
<span class="kw">except</span>:
|
|
2334
|
+
verdict = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
|
|
2335
|
+
|
|
2336
|
+
<span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
|
|
2337
|
+
<span class="kw">continue</span>
|
|
2338
|
+
<span class="kw">elif</span> verdict[<span class="str">"decision"</span>] == <span class="str">"MODIFY"</span>:
|
|
2339
|
+
raw_action = verdict.get(<span class="str">"modified_action"</span>, raw_action)
|
|
2340
|
+
|
|
2341
|
+
actions[agent] = raw_action
|
|
2342
|
+
|
|
2343
|
+
<span class="kw">await</span> result.env.step(actions)</div>
|
|
2344
|
+
<div style="font-size:9px;color:var(--text-muted);margin-top:6px;line-height:1.5">
|
|
2345
|
+
Apply to all 3 scripts: <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_twitter_simulation.py</code>
|
|
2346
|
+
<code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_reddit_simulation.py</code>
|
|
2347
|
+
<code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_parallel_simulation.py</code>
|
|
2348
|
+
</div>
|
|
944
2349
|
</div>
|
|
945
|
-
|
|
946
|
-
|
|
2350
|
+
</div>
|
|
2351
|
+
|
|
2352
|
+
<div class="fw-panel" id="fw-langchain">
|
|
2353
|
+
<div class="integrate-section">
|
|
2354
|
+
<h4>Wrap your tool or agent executor</h4>
|
|
2355
|
+
<div class="integrate-code"><span class="kw">import</span> requests
|
|
2356
|
+
<span class="kw">from</span> langchain.tools <span class="kw">import</span> tool
|
|
2357
|
+
|
|
2358
|
+
<span class="kw">def</span> governed(fn, world=<span class="str">"trading"</span>):
|
|
2359
|
+
<span class="kw">def</span> wrapper(*args, **kwargs):
|
|
2360
|
+
action = fn.__name__
|
|
2361
|
+
<span class="kw">try</span>:
|
|
2362
|
+
v = requests.post(
|
|
2363
|
+
<span class="str">"http://localhost:3456/api/evaluate"</span>,
|
|
2364
|
+
json={<span class="str">"actor"</span>: <span class="str">"langchain"</span>,
|
|
2365
|
+
<span class="str">"action"</span>: action,
|
|
2366
|
+
<span class="str">"world"</span>: world},
|
|
2367
|
+
timeout=0.5
|
|
2368
|
+
).json()
|
|
2369
|
+
<span class="kw">except</span>:
|
|
2370
|
+
v = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
|
|
2371
|
+
<span class="kw">if</span> v[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
|
|
2372
|
+
<span class="kw">return</span> f<span class="str">"Blocked: {v['reason']}"</span>
|
|
2373
|
+
<span class="kw">return</span> fn(*args, **kwargs)
|
|
2374
|
+
<span class="kw">return</span> wrapper
|
|
2375
|
+
|
|
2376
|
+
<span class="comment"># Usage: wrap any tool</span>
|
|
2377
|
+
@tool
|
|
2378
|
+
@governed
|
|
2379
|
+
<span class="kw">def</span> execute_trade(ticker, amount):
|
|
2380
|
+
...</div>
|
|
947
2381
|
</div>
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
2382
|
+
</div>
|
|
2383
|
+
|
|
2384
|
+
<div style="margin-top:8px">
|
|
2385
|
+
<div class="integrate-hint">
|
|
2386
|
+
<span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> stopped
|
|
2387
|
+
<span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> adjusted
|
|
951
2388
|
<span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
|
|
952
2389
|
</div>
|
|
2390
|
+
<div style="font-size:10px;color:var(--text-faint);margin-top:8px;border-top:1px solid var(--border);padding-top:6px">
|
|
2391
|
+
The world file controls everything. Same rules govern simulated and live systems.
|
|
2392
|
+
</div>
|
|
2393
|
+
</div>
|
|
2394
|
+
</div>
|
|
2395
|
+
|
|
2396
|
+
<!-- Session Report Panel -->
|
|
2397
|
+
<div class="ctrl-section" id="session-panel">
|
|
2398
|
+
<h3 class="ctrl-label">SESSION</h3>
|
|
2399
|
+
<div class="metric-grid" style="grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
|
|
2400
|
+
<div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
|
|
2401
|
+
<div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
|
|
2402
|
+
<div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
|
|
2403
|
+
<div class="metric-box"><div class="value" id="s-allowed" style="color:#4ade80">0</div><div class="label">Allowed</div></div>
|
|
2404
|
+
</div>
|
|
2405
|
+
<div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
|
|
2406
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
2407
|
+
<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>
|
|
2408
|
+
<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>
|
|
2409
|
+
<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>
|
|
2410
|
+
</div>
|
|
2411
|
+
<div id="session-history" style="margin-top:6px;font-size:10px;color:#666"></div>
|
|
953
2412
|
</div>
|
|
954
2413
|
</div>
|
|
955
2414
|
</div>
|
|
956
2415
|
|
|
957
|
-
<!-- RIGHT: VIEWER -->
|
|
958
|
-
<div class="viewer">
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
<
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
2416
|
+
<!-- RIGHT: VIEWER -->
|
|
2417
|
+
<div class="viewer">
|
|
2418
|
+
|
|
2419
|
+
<!-- LAYER 1: OUTCOME -->
|
|
2420
|
+
<div class="outcome-panel" id="outcome-panel">
|
|
2421
|
+
<div class="outcome-statement" id="outcome-statement">
|
|
2422
|
+
<span class="outcome-empty">Run a simulation or connect an agent to see outcomes</span>
|
|
2423
|
+
</div>
|
|
2424
|
+
<div class="confidence-grid" id="confidence-grid" style="display:none">
|
|
2425
|
+
<div class="confidence-card">
|
|
2426
|
+
<div class="cc-label">Conclusion Strength</div>
|
|
2427
|
+
<div class="cc-value" id="cc-strength">--</div>
|
|
2428
|
+
</div>
|
|
2429
|
+
<div class="confidence-card">
|
|
2430
|
+
<div class="cc-label">Evidence Quality</div>
|
|
2431
|
+
<div class="cc-value" id="cc-evidence">--</div>
|
|
2432
|
+
</div>
|
|
2433
|
+
<div class="confidence-card">
|
|
2434
|
+
<div class="cc-label">Risk of Error</div>
|
|
2435
|
+
<div class="cc-value" id="cc-risk">--</div>
|
|
2436
|
+
</div>
|
|
2437
|
+
</div>
|
|
2438
|
+
<div class="outcome-context" id="outcome-context"></div>
|
|
2439
|
+
</div>
|
|
2440
|
+
|
|
2441
|
+
<!-- LAYER 2: BEHAVIOR -->
|
|
2442
|
+
<div class="behavior-panel" id="behavior-panel-v2">
|
|
2443
|
+
<h2>What Agents Did</h2>
|
|
2444
|
+
<div class="behavior-shifts" id="behavior-shifts">
|
|
2445
|
+
<div class="behavior-empty">Waiting for agent actions</div>
|
|
2446
|
+
</div>
|
|
2447
|
+
<div class="activity-toggle" id="activity-toggle" onclick="document.getElementById('activity-timeline').classList.toggle('open');this.querySelector('.arrow').textContent=document.getElementById('activity-timeline').classList.contains('open')?'▼':'▶'" style="display:none">
|
|
2448
|
+
<span class="arrow">▶</span> See activity timeline
|
|
2449
|
+
</div>
|
|
2450
|
+
<div class="activity-timeline" id="activity-timeline"></div>
|
|
2451
|
+
</div>
|
|
2452
|
+
|
|
2453
|
+
<!-- LAYER 3: WHY -->
|
|
2454
|
+
<div class="why-panel" id="why-panel">
|
|
2455
|
+
<h2>Why This Happened</h2>
|
|
2456
|
+
<div id="why-content">
|
|
2457
|
+
<div class="why-empty">Causation analysis appears after agent actions are evaluated</div>
|
|
2458
|
+
</div>
|
|
2459
|
+
</div>
|
|
2460
|
+
|
|
2461
|
+
<!-- LAYER 4: EXPORT -->
|
|
2462
|
+
<div class="export-bar">
|
|
2463
|
+
<button class="export-btn" onclick="exportPDF()">Download PDF</button>
|
|
2464
|
+
<button class="export-btn" onclick="exportCSV()">Export CSV</button>
|
|
2465
|
+
<button class="export-btn" onclick="copyShareSummary()">Copy Summary</button>
|
|
2466
|
+
<button class="export-btn audit-btn" onclick="openAudit()">Audit Trail</button>
|
|
2467
|
+
</div>
|
|
2468
|
+
|
|
2469
|
+
<!-- Hidden elements for data tracking (not displayed) -->
|
|
2470
|
+
<div style="display:none">
|
|
2471
|
+
<span id="m-stability">--</span>
|
|
2472
|
+
<span id="m-volatility">--</span>
|
|
2473
|
+
<span id="m-round">--</span>
|
|
2474
|
+
<span id="m-interventions">0</span>
|
|
2475
|
+
<span id="trace-source"></span>
|
|
2476
|
+
<div id="agents"></div>
|
|
2477
|
+
<div id="active-invariants"></div>
|
|
2478
|
+
<div id="log"></div>
|
|
2479
|
+
</div>
|
|
2480
|
+
</div>
|
|
2481
|
+
|
|
2482
|
+
<!-- LAYER 5: AUDIT OVERLAY (separate from dashboard) -->
|
|
2483
|
+
<div class="audit-overlay" id="audit-overlay" onclick="if(event.target===this)closeAudit()">
|
|
2484
|
+
<div class="audit-modal">
|
|
2485
|
+
<div class="audit-header">
|
|
2486
|
+
<h2>Audit Trail</h2>
|
|
2487
|
+
<button class="audit-close" onclick="closeAudit()">Close</button>
|
|
2488
|
+
</div>
|
|
2489
|
+
<div class="audit-section" id="audit-rules">
|
|
2490
|
+
<h3>Active Rules</h3>
|
|
2491
|
+
<div id="audit-rules-content"></div>
|
|
2492
|
+
</div>
|
|
2493
|
+
<div class="audit-section" id="audit-verdicts">
|
|
2494
|
+
<h3>Verdict Log</h3>
|
|
2495
|
+
<div id="audit-verdicts-content"></div>
|
|
2496
|
+
</div>
|
|
2497
|
+
<div class="audit-section" id="audit-trace">
|
|
2498
|
+
<h3>Detailed Trace</h3>
|
|
2499
|
+
<div id="audit-trace-content"></div>
|
|
2500
|
+
</div>
|
|
2501
|
+
</div>
|
|
2502
|
+
</div>
|
|
2503
|
+
</div>
|
|
2504
|
+
|
|
2505
|
+
<script>
|
|
2506
|
+
// ============================================
|
|
2507
|
+
// LANGUAGE & FRAMEWORK TABS
|
|
2508
|
+
// ============================================
|
|
2509
|
+
function switchLang(lang, btn) {
|
|
2510
|
+
const container = btn.closest('.integrate-section') || document;
|
|
2511
|
+
container.querySelectorAll('.lang-tab').forEach(t => t.classList.remove('active'));
|
|
2512
|
+
container.querySelectorAll('.lang-code-panel').forEach(p => p.classList.remove('active'));
|
|
2513
|
+
btn.classList.add('active');
|
|
2514
|
+
const panel = document.getElementById('lang-' + lang);
|
|
2515
|
+
if (panel) panel.classList.add('active');
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
function switchFw(fw, btn) {
|
|
2519
|
+
document.querySelectorAll('.fw-tab').forEach(t => t.classList.remove('active'));
|
|
2520
|
+
document.querySelectorAll('.fw-panel').forEach(p => p.classList.remove('active'));
|
|
2521
|
+
btn.classList.add('active');
|
|
2522
|
+
const panel = document.getElementById('fw-' + fw);
|
|
2523
|
+
if (panel) panel.classList.add('active');
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// ============================================
|
|
2527
|
+
// OUTCOME INTELLIGENCE LAYER
|
|
2528
|
+
// ============================================
|
|
2529
|
+
|
|
2530
|
+
function generateOutcome(stability, volatility, shiftData, worldThesis) {
|
|
2531
|
+
var st = stability || 0;
|
|
2532
|
+
var vol = volatility || 0;
|
|
2533
|
+
var blockRatio = shiftData.total > 0 ? shiftData.blocks / shiftData.total : 0;
|
|
2534
|
+
var thesis = worldThesis || '';
|
|
2535
|
+
|
|
2536
|
+
// Determine outcome direction + dominant behavior (state + what agents did)
|
|
2537
|
+
var direction = '';
|
|
2538
|
+
var behavior = '';
|
|
2539
|
+
if (st > 0.7 && vol < 0.3) {
|
|
2540
|
+
direction = 'stabilized';
|
|
2541
|
+
behavior = 'agents shifted toward safer positions';
|
|
2542
|
+
} else if (st > 0.7 && vol >= 0.3) {
|
|
2543
|
+
direction = 'held under pressure';
|
|
2544
|
+
behavior = 'agents maintained cautious strategies despite volatility';
|
|
2545
|
+
} else if (st > 0.4 && vol < 0.5) {
|
|
2546
|
+
direction = 'partially converged';
|
|
2547
|
+
behavior = 'agents split between aggressive and conservative approaches';
|
|
2548
|
+
} else if (st <= 0.4 && vol >= 0.5) {
|
|
2549
|
+
direction = 'fragmented';
|
|
2550
|
+
behavior = 'agents competed with conflicting strategies';
|
|
2551
|
+
} else if (st <= 0.4) {
|
|
2552
|
+
direction = 'remains uncertain';
|
|
2553
|
+
behavior = 'agents failed to find a dominant strategy';
|
|
2554
|
+
} else {
|
|
2555
|
+
direction = 'showed mixed results';
|
|
2556
|
+
behavior = 'agents oscillated between risk-taking and caution';
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// Enrich with world context if available
|
|
2560
|
+
if (thesis) {
|
|
2561
|
+
var nouns = thesis.match(/\b(market|research|supply|trade|financial|climate|regulatory|investment|innovation|security)\b/i);
|
|
2562
|
+
if (nouns) {
|
|
2563
|
+
var subject = nouns[1].charAt(0).toUpperCase() + nouns[1].slice(1).toLowerCase();
|
|
2564
|
+
return subject + ' ' + direction + ' as ' + behavior;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// Build behavior-enriched statement — always state + dominant behavior
|
|
2569
|
+
if (blockRatio > 0.4 && st > 0.6) return 'System ' + direction + ' after agents abandoned high-risk strategies';
|
|
2570
|
+
if (blockRatio > 0.2) return 'Outcome ' + direction + ' as ' + behavior;
|
|
2571
|
+
if (shiftData.patterns && shiftData.patterns.length > 0) return 'Outcome ' + direction + ' — ' + behavior;
|
|
2572
|
+
return 'Outcome ' + direction + ' as ' + behavior;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function computeConfidence(stability, volatility, interventions, total) {
|
|
2576
|
+
var interventionRate = total > 0 ? interventions / total : 0;
|
|
2577
|
+
var score = stability * 0.5 + (1 - volatility) * 0.3 + (1 - interventionRate) * 0.2;
|
|
2578
|
+
|
|
2579
|
+
var strength, evidence, risk, strengthCls, evidenceCls, riskCls;
|
|
2580
|
+
|
|
2581
|
+
if (score > 0.75) {
|
|
2582
|
+
strength = 'Strong'; strengthCls = 'good';
|
|
2583
|
+
evidence = 'Solid'; evidenceCls = 'good';
|
|
2584
|
+
risk = 'Low'; riskCls = 'good';
|
|
2585
|
+
} else if (score > 0.55) {
|
|
2586
|
+
strength = 'Moderate'; strengthCls = 'warn';
|
|
2587
|
+
evidence = stability > 0.6 ? 'Solid' : 'Mixed'; evidenceCls = stability > 0.6 ? 'good' : 'warn';
|
|
2588
|
+
risk = volatility > 0.4 ? 'Elevated' : 'Moderate'; riskCls = volatility > 0.4 ? 'warn' : 'warn';
|
|
2589
|
+
} else if (score > 0.35) {
|
|
2590
|
+
strength = 'Moderate'; strengthCls = 'warn';
|
|
2591
|
+
evidence = 'Mixed'; evidenceCls = 'warn';
|
|
2592
|
+
risk = 'Elevated'; riskCls = 'bad';
|
|
2593
|
+
} else {
|
|
2594
|
+
strength = 'Weak'; strengthCls = 'bad';
|
|
2595
|
+
evidence = 'Thin'; evidenceCls = 'bad';
|
|
2596
|
+
risk = 'High'; riskCls = 'bad';
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
return { score: score, strength: strength, strengthCls: strengthCls, evidence: evidence, evidenceCls: evidenceCls, risk: risk, riskCls: riskCls };
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function translateBehaviorNarrative(reaction, verdict) {
|
|
2603
|
+
// Convert verdict+action into before → after narrative — no system words
|
|
2604
|
+
var status = verdict ? verdict.status : 'ALLOW';
|
|
2605
|
+
var action = reaction || 'acted';
|
|
2606
|
+
if (status === 'BLOCK') {
|
|
2607
|
+
return 'abandoned ' + action + ' and switched to a safer strategy';
|
|
2608
|
+
}
|
|
2609
|
+
if (status === 'MODIFY' || status === 'PAUSE') {
|
|
2610
|
+
return 'scaled back ' + action + ' after early resistance';
|
|
2611
|
+
}
|
|
2612
|
+
return action;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
function generateBehaviorShifts(shiftData, bLog) {
|
|
2616
|
+
// Quantified behavioral shifts in human language
|
|
2617
|
+
var sentences = [];
|
|
2618
|
+
var total = shiftData.total || bLog.length || 0;
|
|
2619
|
+
if (total === 0) return sentences;
|
|
2620
|
+
|
|
2621
|
+
// Group by what agents actually did — always anchor in before → after
|
|
2622
|
+
var keys = Object.keys(shiftData.shifts || {}).sort(function(a, b) { return shiftData.shifts[b] - shiftData.shifts[a]; });
|
|
2623
|
+
if (keys.length > 0 && total > 0) {
|
|
2624
|
+
keys.slice(0, 3).forEach(function(k) {
|
|
2625
|
+
var parts = k.split(': ');
|
|
2626
|
+
var action = parts[1] || parts[0];
|
|
2627
|
+
var count = shiftData.shifts[k];
|
|
2628
|
+
var pct = Math.round((count / total) * 100);
|
|
2629
|
+
// before → after: what they tried → what they did instead
|
|
2630
|
+
if (parts[0] === 'BLOCK') {
|
|
2631
|
+
sentences.push(pct + '% of agents (' + count + ') shifted from ' + action + ' to conservative strategies');
|
|
2632
|
+
} else {
|
|
2633
|
+
sentences.push(pct + '% of agents (' + count + ') reduced ' + action + ' after initial attempts failed');
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// Fallback from behavioral log — still before → after
|
|
2639
|
+
if (sentences.length === 0 && bLog.length > 0) {
|
|
2640
|
+
var blocked = bLog.filter(function(e) { return e.status === 'BLOCK'; }).length;
|
|
2641
|
+
var modified = bLog.filter(function(e) { return e.status === 'MODIFY' || e.status === 'PAUSE'; }).length;
|
|
2642
|
+
var proceeded = bLog.length - blocked - modified;
|
|
2643
|
+
|
|
2644
|
+
if (blocked > 0) {
|
|
2645
|
+
var bPct = Math.round((blocked / bLog.length) * 100);
|
|
2646
|
+
sentences.push(bPct + '% of agents (' + blocked + ') shifted from aggressive to conservative strategies');
|
|
2647
|
+
}
|
|
2648
|
+
if (modified > 0) {
|
|
2649
|
+
var mPct = Math.round((modified / bLog.length) * 100);
|
|
2650
|
+
sentences.push(mPct + '% of agents (' + modified + ') reduced position size after initial attempts failed');
|
|
2651
|
+
}
|
|
2652
|
+
if (proceeded > 0 && (blocked > 0 || modified > 0)) {
|
|
2653
|
+
var pPct = Math.round((proceeded / bLog.length) * 100);
|
|
2654
|
+
sentences.push(pPct + '% of agents (' + proceeded + ') maintained their original strategy throughout');
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
// Volatility insight — before → after framing
|
|
2659
|
+
if (shiftData.governedVol < shiftData.baselineVol && shiftData.baselineVol > 0) {
|
|
2660
|
+
var volDrop = Math.round((1 - shiftData.governedVol / shiftData.baselineVol) * 100);
|
|
2661
|
+
sentences.push('Uncertainty dropped ' + volDrop + '% as agents moved from exploration to caution');
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// Add emergent pattern insights
|
|
2665
|
+
if (shiftData.patterns && shiftData.patterns.length > 0) {
|
|
2666
|
+
shiftData.patterns.slice(0, 2).forEach(function(p) {
|
|
2667
|
+
sentences.push(p);
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
return sentences;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
function generateCausation(shiftData, bLog) {
|
|
2675
|
+
// Pure causation — why behavior changed — no governance terminology
|
|
2676
|
+
var causes = [];
|
|
2677
|
+
var total = shiftData.total || bLog.length || 0;
|
|
2678
|
+
if (total === 0) return causes;
|
|
2679
|
+
|
|
2680
|
+
var blockRatio = shiftData.blocks / total;
|
|
2681
|
+
|
|
2682
|
+
// No system words (feedback, evaluation, validation, review)
|
|
2683
|
+
// Only agent experience (failed attempts, uncertainty, risk, delay)
|
|
2684
|
+
if (blockRatio > 0.4) {
|
|
2685
|
+
causes.push('Early aggressive attempts failed, forcing agents to rethink their strategy');
|
|
2686
|
+
} else if (blockRatio > 0.1) {
|
|
2687
|
+
causes.push('Some agents hit unexpected resistance and pulled back');
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
if (shiftData.governedVol < shiftData.baselineVol) {
|
|
2691
|
+
causes.push('Uncertainty dropped as agents stopped experimenting and committed to safer positions');
|
|
2692
|
+
}
|
|
974
2693
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
<h2>Impact Timeline</h2>
|
|
984
|
-
<div class="chart-container"><canvas id="chart"></canvas></div>
|
|
985
|
-
</div>
|
|
986
|
-
</div>
|
|
2694
|
+
if (shiftData.patterns && shiftData.patterns.length > 0) {
|
|
2695
|
+
if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('hold') || p.toLowerCase().includes('caution'); })) {
|
|
2696
|
+
causes.push('Risk became too visible — agents chose to wait rather than act');
|
|
2697
|
+
}
|
|
2698
|
+
if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('coordination') || p.toLowerCase().includes('coordinated'); })) {
|
|
2699
|
+
causes.push('Agents independently converged on similar strategies under shared pressure');
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
987
2702
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
<span>Rule</span><span class="ss-flow-arrow">→</span>
|
|
998
|
-
<span>Behavioral Shift</span><span class="ss-flow-arrow">→</span>
|
|
999
|
-
<span>Emergent Pattern</span><span class="ss-flow-arrow">→</span>
|
|
1000
|
-
<span>System Outcome</span>
|
|
1001
|
-
</div>
|
|
1002
|
-
<div class="ss-body">
|
|
1003
|
-
<div class="ss-section">
|
|
1004
|
-
<div class="ss-section-label">Behavioral Shift</div>
|
|
1005
|
-
<div class="ss-adapt-rate" id="ss-adapt-rate"></div>
|
|
1006
|
-
<div class="ss-adapt-desc" id="ss-adapt-desc"></div>
|
|
1007
|
-
<div id="ss-shifts"></div>
|
|
1008
|
-
</div>
|
|
1009
|
-
<div class="ss-section">
|
|
1010
|
-
<div class="ss-section-label">What Emerged</div>
|
|
1011
|
-
<div id="ss-patterns"></div>
|
|
1012
|
-
</div>
|
|
1013
|
-
<div class="ss-section">
|
|
1014
|
-
<div class="ss-section-label">System Outcome</div>
|
|
1015
|
-
<div id="ss-impacts"></div>
|
|
1016
|
-
</div>
|
|
1017
|
-
<div class="ss-section">
|
|
1018
|
-
<div class="ss-section-label">What Actually Happened</div>
|
|
1019
|
-
<div class="ss-narrative" id="ss-narrative"></div>
|
|
1020
|
-
</div>
|
|
1021
|
-
</div>
|
|
1022
|
-
<button class="ss-raw-toggle" id="ss-raw-toggle">
|
|
1023
|
-
<span class="arrow">▶</span> View raw detail
|
|
1024
|
-
</button>
|
|
1025
|
-
<div class="ss-raw-detail" id="ss-raw-detail">
|
|
1026
|
-
<div class="ss-raw-list" id="ss-raw-list"></div>
|
|
1027
|
-
</div>
|
|
1028
|
-
</div>
|
|
2703
|
+
// Detect adaptation pattern from behavior log
|
|
2704
|
+
if (bLog.length >= 3) {
|
|
2705
|
+
var recent = bLog.slice(-6);
|
|
2706
|
+
var earlyBlocks = recent.slice(0, 3).filter(function(e) { return e.status === 'BLOCK'; }).length;
|
|
2707
|
+
var lateAllows = recent.slice(-3).filter(function(e) { return e.status === 'ALLOW'; }).length;
|
|
2708
|
+
if (earlyBlocks >= 2 && lateAllows >= 2) {
|
|
2709
|
+
causes.push('Agents became more cautious after early attempts failed');
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
1029
2712
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
2713
|
+
if (causes.length === 0) {
|
|
2714
|
+
causes.push('Agents held steady — no major shifts in strategy throughout the simulation');
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
return causes;
|
|
2718
|
+
}
|
|
1036
2719
|
|
|
1037
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"><\/script>
|
|
1038
|
-
<script>
|
|
1039
2720
|
// ============================================
|
|
1040
2721
|
// STATE
|
|
1041
2722
|
// ============================================
|
|
@@ -1045,11 +2726,12 @@ let narratives = {};
|
|
|
1045
2726
|
let currentWorld = null;
|
|
1046
2727
|
let injectedEvents = [];
|
|
1047
2728
|
let totalInterventions = 0;
|
|
1048
|
-
let
|
|
1049
|
-
let governedImpacts = [];
|
|
1050
|
-
let chartLabels = [];
|
|
1051
|
-
let chart = null;
|
|
2729
|
+
let totalActions = 0;
|
|
1052
2730
|
let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
|
|
2731
|
+
let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
|
|
2732
|
+
let behaviorLog = []; // { agent, action, status, reason, ts }
|
|
2733
|
+
let latestStability = 0;
|
|
2734
|
+
let latestVolatility = 0;
|
|
1053
2735
|
|
|
1054
2736
|
const statusEl = document.getElementById('status');
|
|
1055
2737
|
const worldSelect = document.getElementById('world-select');
|
|
@@ -1068,6 +2750,79 @@ const activeInvEl = document.getElementById('active-invariants');
|
|
|
1068
2750
|
const engineSelect = document.getElementById('engine-select');
|
|
1069
2751
|
const engineStatusEl = document.getElementById('engine-status');
|
|
1070
2752
|
const traceSourceEl = document.getElementById('trace-source');
|
|
2753
|
+
const outcomeStatementEl = document.getElementById('outcome-statement');
|
|
2754
|
+
const confidenceGridEl = document.getElementById('confidence-grid');
|
|
2755
|
+
const outcomeContextEl = document.getElementById('outcome-context');
|
|
2756
|
+
const behaviorShiftsEl = document.getElementById('behavior-shifts');
|
|
2757
|
+
const activityTimelineEl = document.getElementById('activity-timeline');
|
|
2758
|
+
const activityToggleEl = document.getElementById('activity-toggle');
|
|
2759
|
+
const whyContentEl = document.getElementById('why-content');
|
|
2760
|
+
|
|
2761
|
+
// Audit + Export
|
|
2762
|
+
function openAudit() {
|
|
2763
|
+
// Populate audit content from current data
|
|
2764
|
+
var rulesEl = document.getElementById('audit-rules-content');
|
|
2765
|
+
rulesEl.innerHTML = activeInvEl.innerHTML || '<div style="font-size:11px;color:#666">No rules loaded</div>';
|
|
2766
|
+
|
|
2767
|
+
var verdictsEl = document.getElementById('audit-verdicts-content');
|
|
2768
|
+
var vHtml = '';
|
|
2769
|
+
behaviorLog.forEach(function(e) {
|
|
2770
|
+
vHtml += '<div style="font-size:10px;padding:2px 0;display:flex;gap:8px;border-bottom:1px solid var(--border)">' +
|
|
2771
|
+
'<span style="color:var(--blue);min-width:100px">' + e.agent + '</span>' +
|
|
2772
|
+
'<span style="flex:1;color:var(--text-secondary)">' + e.action + '</span>' +
|
|
2773
|
+
'<span style="font-size:9px;font-weight:600;padding:0 4px;border-radius:2px;' +
|
|
2774
|
+
(e.status === 'BLOCK' ? 'background:var(--red-bg);color:var(--red)' : e.status === 'ALLOW' ? 'background:var(--green-bg);color:var(--green)' : 'background:var(--yellow-bg);color:var(--yellow)') + '">' + e.status + '</span>' +
|
|
2775
|
+
(e.reason ? '<span style="font-size:9px;color:var(--text-faint)">' + e.reason + '</span>' : '') +
|
|
2776
|
+
'</div>';
|
|
2777
|
+
});
|
|
2778
|
+
verdictsEl.innerHTML = vHtml || '<div style="font-size:11px;color:#666">No verdict data</div>';
|
|
2779
|
+
|
|
2780
|
+
var traceEl = document.getElementById('audit-trace-content');
|
|
2781
|
+
traceEl.innerHTML = logEl.innerHTML || '<div style="font-size:11px;color:#666">No trace data</div>';
|
|
2782
|
+
|
|
2783
|
+
document.getElementById('audit-overlay').classList.add('open');
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
function closeAudit() {
|
|
2787
|
+
document.getElementById('audit-overlay').classList.remove('open');
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
function exportPDF() {
|
|
2791
|
+
window.print();
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function exportCSV() {
|
|
2795
|
+
if (behaviorLog.length === 0) { alert('No data to export yet'); return; }
|
|
2796
|
+
var csv = 'Agent,Action,Behavior,Round\\n';
|
|
2797
|
+
behaviorLog.forEach(function(e) {
|
|
2798
|
+
var behavior = translateBehaviorNarrative(e.action, { status: e.status });
|
|
2799
|
+
csv += '"' + e.agent + '","' + e.action + '","' + behavior + '","' + (e.round || '') + '"\\n';
|
|
2800
|
+
});
|
|
2801
|
+
var blob = new Blob([csv], { type: 'text/csv' });
|
|
2802
|
+
var url = URL.createObjectURL(blob);
|
|
2803
|
+
var a = document.createElement('a');
|
|
2804
|
+
a.href = url;
|
|
2805
|
+
a.download = 'simulation-behavior-' + new Date().toISOString().slice(0, 10) + '.csv';
|
|
2806
|
+
a.click();
|
|
2807
|
+
URL.revokeObjectURL(url);
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
function copyShareSummary() {
|
|
2811
|
+
var outcome = outcomeStatementEl.textContent;
|
|
2812
|
+
var shifts = [];
|
|
2813
|
+
behaviorShiftsEl.querySelectorAll('.behavior-shift-item').forEach(function(el) { shifts.push('- ' + el.textContent); });
|
|
2814
|
+
var causes = [];
|
|
2815
|
+
whyContentEl.querySelectorAll('.why-item').forEach(function(el) { causes.push('- ' + el.textContent); });
|
|
2816
|
+
|
|
2817
|
+
var text = 'OUTCOME: ' + outcome + '\\n\\n';
|
|
2818
|
+
if (shifts.length) text += 'BEHAVIOR:\\n' + shifts.join('\\n') + '\\n\\n';
|
|
2819
|
+
if (causes.length) text += 'WHY:\\n' + causes.join('\\n') + '\\n';
|
|
2820
|
+
text += '\\nGenerated by NeuroVerse Simulations';
|
|
2821
|
+
|
|
2822
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
2823
|
+
alert('Summary copied to clipboard');
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
1071
2826
|
|
|
1072
2827
|
// ============================================
|
|
1073
2828
|
// INIT — Load worlds, scenarios, narratives, adapters
|
|
@@ -1123,6 +2878,9 @@ async function init() {
|
|
|
1123
2878
|
// Load saved variants
|
|
1124
2879
|
await loadVariants();
|
|
1125
2880
|
|
|
2881
|
+
// Populate base world selector for custom rules mode
|
|
2882
|
+
populateBaseWorldSelect();
|
|
2883
|
+
|
|
1126
2884
|
// Connect SSE
|
|
1127
2885
|
connectSSE();
|
|
1128
2886
|
}
|
|
@@ -1235,10 +2993,6 @@ runBtn.addEventListener('click', async () => {
|
|
|
1235
2993
|
|
|
1236
2994
|
// Reset viewer state
|
|
1237
2995
|
totalInterventions = 0;
|
|
1238
|
-
baselineImpacts = [];
|
|
1239
|
-
governedImpacts = [];
|
|
1240
|
-
chartLabels = [];
|
|
1241
|
-
if (chart) { chart.destroy(); chart = null; }
|
|
1242
2996
|
agentsEl.innerHTML = '';
|
|
1243
2997
|
logEl.innerHTML = '';
|
|
1244
2998
|
document.getElementById('m-stability').textContent = '--';
|
|
@@ -1306,28 +3060,7 @@ function connectSSE() {
|
|
|
1306
3060
|
}
|
|
1307
3061
|
|
|
1308
3062
|
function initChart() {
|
|
1309
|
-
|
|
1310
|
-
const ctx = document.getElementById('chart');
|
|
1311
|
-
chart = new Chart(ctx, {
|
|
1312
|
-
type: 'line',
|
|
1313
|
-
data: {
|
|
1314
|
-
labels: chartLabels,
|
|
1315
|
-
datasets: [
|
|
1316
|
-
{ label: 'Baseline', data: baselineImpacts, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
|
|
1317
|
-
{ label: 'Governed', data: governedImpacts, borderColor: '#4ade80', backgroundColor: 'rgba(74,222,128,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
|
|
1318
|
-
]
|
|
1319
|
-
},
|
|
1320
|
-
options: {
|
|
1321
|
-
animation: { duration: 400 },
|
|
1322
|
-
responsive: true,
|
|
1323
|
-
maintainAspectRatio: false,
|
|
1324
|
-
plugins: { legend: { labels: { color: '#888', font: { family: 'monospace', size: 10 } } } },
|
|
1325
|
-
scales: {
|
|
1326
|
-
x: { ticks: { color: '#555' }, grid: { color: '#1a1a1a' } },
|
|
1327
|
-
y: { ticks: { color: '#555' }, grid: { color: '#1a1a1a' }, min: -1, max: 1 }
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
});
|
|
3063
|
+
// Chart removed — no longer needed in outcome-first view
|
|
1331
3064
|
}
|
|
1332
3065
|
|
|
1333
3066
|
function addLog(msg, cls) {
|
|
@@ -1438,6 +3171,84 @@ function renderAgents(reactions) {
|
|
|
1438
3171
|
agentsEl.innerHTML = html;
|
|
1439
3172
|
}
|
|
1440
3173
|
|
|
3174
|
+
// ============================================
|
|
3175
|
+
// PANEL RENDERING — Outcome / Behavior / Why
|
|
3176
|
+
// ============================================
|
|
3177
|
+
|
|
3178
|
+
function recordBehavior(reactions, round) {
|
|
3179
|
+
if (!reactions || !reactions.length) return;
|
|
3180
|
+
reactions.forEach(function(r) {
|
|
3181
|
+
var status = r.verdict ? r.verdict.status : 'ALLOW';
|
|
3182
|
+
var reason = r.verdict ? (r.verdict.reason || '') : '';
|
|
3183
|
+
behaviorLog.push({
|
|
3184
|
+
agent: r.stakeholder_id,
|
|
3185
|
+
action: r.reaction || 'acted',
|
|
3186
|
+
status: status,
|
|
3187
|
+
reason: reason,
|
|
3188
|
+
round: round || 0,
|
|
3189
|
+
ts: Date.now(),
|
|
3190
|
+
});
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
function updateOutcomePanel() {
|
|
3195
|
+
var worldThesis = currentWorld ? (currentWorld.thesis || currentWorld.description || '') : '';
|
|
3196
|
+
var outcome = generateOutcome(latestStability, latestVolatility, shiftTracker, worldThesis);
|
|
3197
|
+
outcomeStatementEl.textContent = outcome;
|
|
3198
|
+
|
|
3199
|
+
// Structured confidence card
|
|
3200
|
+
var conf = computeConfidence(latestStability, latestVolatility, totalInterventions, totalActions);
|
|
3201
|
+
confidenceGridEl.style.display = 'grid';
|
|
3202
|
+
document.getElementById('cc-strength').textContent = conf.strength;
|
|
3203
|
+
document.getElementById('cc-strength').className = 'cc-value ' + conf.strengthCls;
|
|
3204
|
+
document.getElementById('cc-evidence').textContent = conf.evidence;
|
|
3205
|
+
document.getElementById('cc-evidence').className = 'cc-value ' + conf.evidenceCls;
|
|
3206
|
+
document.getElementById('cc-risk').textContent = conf.risk;
|
|
3207
|
+
document.getElementById('cc-risk').className = 'cc-value ' + conf.riskCls;
|
|
3208
|
+
|
|
3209
|
+
// Context line
|
|
3210
|
+
outcomeContextEl.textContent = 'Based on ' + totalActions + ' agent action' + (totalActions !== 1 ? 's' : '') +
|
|
3211
|
+
(shiftTracker.total > 0 ? ' across ' + (document.getElementById('m-round').textContent || '') : '');
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
function updateBehaviorPanel() {
|
|
3215
|
+
// Quantified behavioral shifts in human language
|
|
3216
|
+
var shifts = generateBehaviorShifts(shiftTracker, behaviorLog);
|
|
3217
|
+
if (shifts.length > 0) {
|
|
3218
|
+
var html = shifts.map(function(s) {
|
|
3219
|
+
return '<div class="behavior-shift-item">' + s + '</div>';
|
|
3220
|
+
}).join('');
|
|
3221
|
+
behaviorShiftsEl.innerHTML = html;
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// Activity timeline (narrative micro-events, expandable)
|
|
3225
|
+
if (behaviorLog.length > 0) {
|
|
3226
|
+
activityToggleEl.style.display = 'block';
|
|
3227
|
+
var recent = behaviorLog.slice(-30).reverse();
|
|
3228
|
+
var tHtml = recent.map(function(e) {
|
|
3229
|
+
var narrative = translateBehaviorNarrative(e.action, { status: e.status });
|
|
3230
|
+
return '<div class="activity-item"><span class="activity-agent">' + e.agent + '</span> ' + narrative + '</div>';
|
|
3231
|
+
}).join('');
|
|
3232
|
+
activityTimelineEl.innerHTML = tHtml;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
function updateWhyPanel() {
|
|
3237
|
+
var causes = generateCausation(shiftTracker, behaviorLog);
|
|
3238
|
+
if (causes.length > 0) {
|
|
3239
|
+
var html = causes.map(function(c) {
|
|
3240
|
+
return '<div class="why-item">' + c + '</div>';
|
|
3241
|
+
}).join('');
|
|
3242
|
+
whyContentEl.innerHTML = html;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
function updateAllPanels() {
|
|
3247
|
+
updateOutcomePanel();
|
|
3248
|
+
updateBehaviorPanel();
|
|
3249
|
+
updateWhyPanel();
|
|
3250
|
+
}
|
|
3251
|
+
|
|
1441
3252
|
function handleEvent(event) {
|
|
1442
3253
|
if (event.type === 'meta') {
|
|
1443
3254
|
statusEl.className = 'status live';
|
|
@@ -1461,47 +3272,118 @@ function handleEvent(event) {
|
|
|
1461
3272
|
if (!narrativeEventsByRound[ev.round]) narrativeEventsByRound[ev.round] = [];
|
|
1462
3273
|
narrativeEventsByRound[ev.round].push(ev);
|
|
1463
3274
|
});
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
3275
|
+
function parseRuleTitle(id, text) {
|
|
3276
|
+
var parts = text.split(/\s*[—–-]{1,}\s*/);
|
|
3277
|
+
var title = parts.length > 1 ? parts[0].trim() : id.replace(/[-_]/g, ' ');
|
|
3278
|
+
var desc = parts.length > 1 ? parts.slice(1).join(' — ').trim() : text;
|
|
3279
|
+
title = title.replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
|
3280
|
+
return { title: title, desc: desc };
|
|
3281
|
+
}
|
|
3282
|
+
function generateWhy(text, type) {
|
|
3283
|
+
var t = text.toLowerCase();
|
|
3284
|
+
if (t.includes('liquidity') || t.includes('drain')) return 'Prevents system collapse from liquidity crises';
|
|
3285
|
+
if (t.includes('panic') || t.includes('cascade')) return 'Stops runaway feedback loops during market shocks';
|
|
3286
|
+
if (t.includes('leverage')) return 'Limits systemic risk from over-leveraged positions';
|
|
3287
|
+
if (t.includes('price') || t.includes('pricing')) return 'Stabilizes pricing mechanisms during volatility';
|
|
3288
|
+
if (t.includes('sentiment') || t.includes('consumer')) return 'Tracks behavioral feedback between price and confidence';
|
|
3289
|
+
if (t.includes('military') || t.includes('escalat')) return 'Models how escalation constrains available options';
|
|
3290
|
+
if (t.includes('diplomatic') || t.includes('window')) return 'Preserves negotiation pathways before they close';
|
|
3291
|
+
if (t.includes('grid') || t.includes('capacity')) return 'Prevents infrastructure overload from demand surges';
|
|
3292
|
+
if (t.includes('election') || t.includes('political')) return 'Captures how political pressure amplifies responses';
|
|
3293
|
+
if (t.includes('supply') || t.includes('energy') || t.includes('oil')) return 'Guards against cascading supply chain disruptions';
|
|
3294
|
+
if (t.includes('fraud') || t.includes('suspicious')) return 'Detects and contains anomalous behavior patterns';
|
|
3295
|
+
if (t.includes('withdraw') || t.includes('limit')) return 'Constrains individual actions to protect system stability';
|
|
3296
|
+
if (type === 'gate') return 'Blocks dangerous actions before they propagate';
|
|
3297
|
+
if (type === 'warning') return 'Provides early warning before thresholds are breached';
|
|
3298
|
+
return 'Maintains system integrity under stress conditions';
|
|
3299
|
+
}
|
|
3300
|
+
// Reset per-rule impact counters
|
|
3301
|
+
ruleImpactTracker = {};
|
|
3302
|
+
activeInvEl.innerHTML = event.invariants.map(function(inv) {
|
|
3303
|
+
var parsed = parseRuleTitle(inv.id, inv.description);
|
|
3304
|
+
var why = generateWhy(inv.description, 'invariant');
|
|
3305
|
+
var isUser = inv.source === 'user';
|
|
3306
|
+
var isFull = inv.enforcement === 'full';
|
|
3307
|
+
var enfLabel = isFull ? 'Fully enforced across system' : 'Advisory only';
|
|
3308
|
+
var enfIcon = isFull ? '✓' : '⚠';
|
|
3309
|
+
var sourceTag = isUser ? '<span class="rule-source-tag">USER RULE</span>' : '';
|
|
3310
|
+
ruleImpactTracker[inv.id] = { blocks: 0, label: parsed.title };
|
|
3311
|
+
return '<div class="rule-card type-invariant' + (isUser ? ' user-rule' : '') + '" data-rule-id="' + inv.id + '">' +
|
|
3312
|
+
'<div class="rule-header"><span class="rule-icon">🟢</span><span class="rule-title">' + parsed.title + '</span>' + sourceTag + '</div>' +
|
|
3313
|
+
'<div class="rule-desc">' + parsed.desc + '</div>' +
|
|
3314
|
+
'<div class="rule-meta">Invariant • ' + enfIcon + ' ' + enfLabel + '</div>' +
|
|
3315
|
+
'<div class="rule-why">' + why + '</div>' +
|
|
3316
|
+
'<div class="rule-impact" data-impact-id="' + inv.id + '"></div>' +
|
|
3317
|
+
'</div>';
|
|
3318
|
+
}).join('') + event.gates.map(function(g) {
|
|
3319
|
+
var isCritical = g.severity === 'critical';
|
|
3320
|
+
var typeClass = isCritical ? 'type-gate' : 'type-warning';
|
|
3321
|
+
var icon = isCritical ? '🔴' : '🟡';
|
|
3322
|
+
var typeLabel = isCritical ? 'Gate' : 'Warning';
|
|
3323
|
+
var effect = isCritical ? 'Blocks actions' : 'Signals risk';
|
|
3324
|
+
var why = generateWhy(g.label + ' ' + (g.condition || ''), isCritical ? 'gate' : 'warning');
|
|
3325
|
+
ruleImpactTracker[g.id] = { blocks: 0, label: g.label };
|
|
3326
|
+
return '<div class="rule-card ' + typeClass + '" data-rule-id="' + g.id + '">' +
|
|
3327
|
+
'<div class="rule-header"><span class="rule-icon">' + icon + '</span><span class="rule-title">' + g.label + '</span></div>' +
|
|
3328
|
+
'<div class="rule-desc">' + (g.condition || g.label) + '</div>' +
|
|
3329
|
+
'<div class="rule-meta">' + typeLabel + ' • ' + effect + '</div>' +
|
|
3330
|
+
'<div class="rule-why">' + why + '</div>' +
|
|
3331
|
+
'<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
|
|
3332
|
+
'</div>';
|
|
3333
|
+
}).join('');
|
|
1470
3334
|
}
|
|
1471
3335
|
|
|
1472
3336
|
if (event.type === 'round') {
|
|
1473
|
-
|
|
1474
|
-
chartLabels.push('R' + event.round);
|
|
1475
|
-
baselineImpacts.push(event.avgImpact);
|
|
1476
|
-
} else {
|
|
1477
|
-
governedImpacts.push(event.avgImpact);
|
|
1478
|
-
}
|
|
1479
|
-
if (chart) chart.update();
|
|
3337
|
+
const isBridge = event.reactions && event.reactions[0] && event.reactions[0].trigger === 'bridge';
|
|
1480
3338
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
document.getElementById('m-volatility').parentElement.className = 'metric-box ' + (event.maxVolatility > 0.6 ? 'bad' : event.maxVolatility > 0.4 ? 'warn' : 'good');
|
|
3339
|
+
// Record behavior data (no governance display)
|
|
3340
|
+
recordBehavior(event.reactions, event.round);
|
|
3341
|
+
renderAgents(event.reactions); // hidden, kept for audit
|
|
1485
3342
|
|
|
3343
|
+
// Track totals
|
|
3344
|
+
totalActions += event.reactions ? event.reactions.length : 0;
|
|
1486
3345
|
totalInterventions += event.interventionCount;
|
|
3346
|
+
latestVolatility = event.maxVolatility || 0;
|
|
3347
|
+
|
|
3348
|
+
// Update hidden data elements for audit
|
|
3349
|
+
if (isBridge) {
|
|
3350
|
+
document.getElementById('m-round').textContent = event.round + ' evals';
|
|
3351
|
+
} else {
|
|
3352
|
+
document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
|
|
3353
|
+
}
|
|
3354
|
+
document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
|
|
1487
3355
|
document.getElementById('m-interventions').textContent = totalInterventions;
|
|
1488
3356
|
|
|
1489
|
-
// Track system shifts
|
|
3357
|
+
// Track system shifts (data collection, not displayed)
|
|
1490
3358
|
trackShift(event);
|
|
1491
3359
|
|
|
1492
|
-
//
|
|
3360
|
+
// Update all visible panels
|
|
3361
|
+
updateAllPanels();
|
|
3362
|
+
|
|
3363
|
+
// Add trace entry for audit
|
|
1493
3364
|
addTraceRound(event);
|
|
1494
3365
|
}
|
|
1495
3366
|
|
|
3367
|
+
// Bridge metrics — updates stability from external /api/evaluate calls
|
|
3368
|
+
if (event.type === 'bridge_metrics') {
|
|
3369
|
+
latestStability = event.stability;
|
|
3370
|
+
document.getElementById('m-stability').textContent = (event.stability * 100).toFixed(0) + '%';
|
|
3371
|
+
updateAllPanels();
|
|
3372
|
+
}
|
|
3373
|
+
|
|
1496
3374
|
if (event.type === 'complete') {
|
|
1497
3375
|
statusEl.className = 'status complete';
|
|
1498
3376
|
statusEl.textContent = 'COMPLETE';
|
|
1499
3377
|
const r = event.result;
|
|
1500
3378
|
if (r.governed) {
|
|
3379
|
+
latestStability = r.governed.metrics.stabilityScore;
|
|
3380
|
+
latestVolatility = r.governed.metrics.maxVolatility || latestVolatility;
|
|
1501
3381
|
document.getElementById('m-stability').textContent = (r.governed.metrics.stabilityScore * 100).toFixed(0) + '%';
|
|
1502
|
-
|
|
1503
|
-
addLog('Complete. Governance effectiveness: ' + (r.comparison.governanceEffectiveness * 100).toFixed(0) + '%');
|
|
3382
|
+
addLog('Simulation complete');
|
|
1504
3383
|
renderSystemShift(r);
|
|
3384
|
+
renderRuleImpacts(r);
|
|
3385
|
+
renderEnforcementClassification(r.enforcementClassification || []);
|
|
3386
|
+
updateAllPanels();
|
|
1505
3387
|
lastSimResult = {
|
|
1506
3388
|
stability: r.governed.metrics.stabilityScore,
|
|
1507
3389
|
volatility: r.governed.metrics.maxVolatility,
|
|
@@ -1514,6 +3396,59 @@ function handleEvent(event) {
|
|
|
1514
3396
|
}
|
|
1515
3397
|
}
|
|
1516
3398
|
|
|
3399
|
+
// ============================================
|
|
3400
|
+
// PER-RULE IMPACT RENDERING
|
|
3401
|
+
// ============================================
|
|
3402
|
+
function renderRuleImpacts(result) {
|
|
3403
|
+
var totalBlocks = shiftTracker.blocks;
|
|
3404
|
+
var cascadeAvoided = result && result.governed && result.baseline &&
|
|
3405
|
+
result.governed.metrics.collapseProbability < result.baseline.metrics.collapseProbability;
|
|
3406
|
+
|
|
3407
|
+
Object.keys(ruleImpactTracker).forEach(function(ruleId) {
|
|
3408
|
+
var tracker = ruleImpactTracker[ruleId];
|
|
3409
|
+
var el = document.querySelector('[data-impact-id="' + ruleId + '"]');
|
|
3410
|
+
if (!el) return;
|
|
3411
|
+
|
|
3412
|
+
var html = '';
|
|
3413
|
+
if (tracker.blocks > 0) {
|
|
3414
|
+
html += '<span class="impact-stat">' + tracker.blocks + ' action' + (tracker.blocks > 1 ? 's' : '') + ' blocked</span>';
|
|
3415
|
+
if (cascadeAvoided) {
|
|
3416
|
+
html += ' <span class="impact-label">→ cascade avoided</span>';
|
|
3417
|
+
} else {
|
|
3418
|
+
html += ' <span class="impact-label">→ behavior modified</span>';
|
|
3419
|
+
}
|
|
3420
|
+
} else {
|
|
3421
|
+
// Rule was present but didn't fire — still useful info
|
|
3422
|
+
html += '<span class="impact-label">No violations detected — agents complied</span>';
|
|
3423
|
+
}
|
|
3424
|
+
el.innerHTML = html;
|
|
3425
|
+
el.classList.add('visible');
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
// ============================================
|
|
3430
|
+
// ENFORCEMENT CLASSIFICATION — Full vs Advisory
|
|
3431
|
+
// ============================================
|
|
3432
|
+
function renderEnforcementClassification(entries) {
|
|
3433
|
+
if (!entries || entries.length === 0) return;
|
|
3434
|
+
entries.forEach(function(entry) {
|
|
3435
|
+
var card = document.querySelector('[data-rule-id="' + entry.id + '"]');
|
|
3436
|
+
if (!card) return;
|
|
3437
|
+
var metaEl = card.querySelector('.rule-meta');
|
|
3438
|
+
if (!metaEl) return;
|
|
3439
|
+
if (entry.level === 'full' && entry.fired) {
|
|
3440
|
+
metaEl.innerHTML = 'Invariant • ✓ Fully enforced • <span style="color:#22c55e;font-weight:600">FIRED</span>';
|
|
3441
|
+
card.style.borderLeftColor = '#22c55e';
|
|
3442
|
+
} else if (entry.level === 'full' && !entry.fired) {
|
|
3443
|
+
metaEl.innerHTML = 'Invariant • ✓ Fully enforced • <span style="color:var(--text-muted)">standby</span>';
|
|
3444
|
+
} else if (entry.level === 'advisory') {
|
|
3445
|
+
metaEl.innerHTML = 'Invariant • ⚠ Advisory only • <span style="color:var(--text-muted)">monitored</span>';
|
|
3446
|
+
card.style.borderLeftColor = '#6b7280';
|
|
3447
|
+
card.style.opacity = '0.75';
|
|
3448
|
+
}
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
|
|
1517
3452
|
// ============================================
|
|
1518
3453
|
// SYSTEM SHIFT CARD — The Demo Moment
|
|
1519
3454
|
// ============================================
|
|
@@ -1527,20 +3462,37 @@ const ssImpactsEl = document.getElementById('ss-impacts');
|
|
|
1527
3462
|
const ssNarrativeEl = document.getElementById('ss-narrative');
|
|
1528
3463
|
|
|
1529
3464
|
// Raw detail toggle
|
|
1530
|
-
document.getElementById('ss-raw-toggle')
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
3465
|
+
var ssRawToggle = document.getElementById('ss-raw-toggle');
|
|
3466
|
+
if (ssRawToggle) {
|
|
3467
|
+
ssRawToggle.addEventListener('click', function() {
|
|
3468
|
+
this.classList.toggle('open');
|
|
3469
|
+
document.getElementById('ss-raw-detail').classList.toggle('open');
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
1534
3472
|
|
|
1535
3473
|
let shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
|
|
1536
3474
|
|
|
1537
3475
|
function resetShiftTracker() {
|
|
1538
3476
|
shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
|
|
1539
|
-
ssCard.classList.remove('visible');
|
|
3477
|
+
if (ssCard) ssCard.classList.remove('visible');
|
|
1540
3478
|
var rawToggle = document.getElementById('ss-raw-toggle');
|
|
1541
3479
|
var rawDetail = document.getElementById('ss-raw-detail');
|
|
1542
3480
|
if (rawToggle) rawToggle.classList.remove('open');
|
|
1543
3481
|
if (rawDetail) rawDetail.classList.remove('open');
|
|
3482
|
+
// Reset behavior tracking
|
|
3483
|
+
behaviorLog = [];
|
|
3484
|
+
totalActions = 0;
|
|
3485
|
+
totalInterventions = 0;
|
|
3486
|
+
latestStability = 0;
|
|
3487
|
+
latestVolatility = 0;
|
|
3488
|
+
// Reset visible panels
|
|
3489
|
+
if (outcomeStatementEl) outcomeStatementEl.innerHTML = '<span class="outcome-empty">Run a simulation or connect an agent to see outcomes</span>';
|
|
3490
|
+
if (confidenceGridEl) confidenceGridEl.style.display = 'none';
|
|
3491
|
+
if (outcomeContextEl) outcomeContextEl.textContent = '';
|
|
3492
|
+
if (behaviorShiftsEl) behaviorShiftsEl.innerHTML = '<div class="behavior-empty">Waiting for agent actions</div>';
|
|
3493
|
+
if (activityToggleEl) activityToggleEl.style.display = 'none';
|
|
3494
|
+
if (activityTimelineEl) { activityTimelineEl.innerHTML = ''; activityTimelineEl.classList.remove('open'); }
|
|
3495
|
+
if (whyContentEl) whyContentEl.innerHTML = '<div class="why-empty">Causation analysis appears after agent actions are evaluated</div>';
|
|
1544
3496
|
}
|
|
1545
3497
|
|
|
1546
3498
|
function trackShift(event) {
|
|
@@ -1551,6 +3503,18 @@ function trackShift(event) {
|
|
|
1551
3503
|
governed.forEach(function(r) {
|
|
1552
3504
|
const key = r.verdict.status + ': ' + (r.reaction || 'adapted');
|
|
1553
3505
|
shiftTracker.shifts[key] = (shiftTracker.shifts[key] || 0) + 1;
|
|
3506
|
+
// Track per-rule impacts for card display
|
|
3507
|
+
var ruleId = r.verdict.ruleId || '';
|
|
3508
|
+
if (ruleId && ruleImpactTracker[ruleId]) {
|
|
3509
|
+
ruleImpactTracker[ruleId].blocks++;
|
|
3510
|
+
} else {
|
|
3511
|
+
// Try to match by pattern — governance may report with NV- prefix
|
|
3512
|
+
Object.keys(ruleImpactTracker).forEach(function(k) {
|
|
3513
|
+
if (ruleId.includes(k) || (r.verdict.reason && r.verdict.reason.toLowerCase().includes(ruleImpactTracker[k].label.toLowerCase()))) {
|
|
3514
|
+
ruleImpactTracker[k].blocks++;
|
|
3515
|
+
}
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
1554
3518
|
// Store raw governed reactions for detail view
|
|
1555
3519
|
shiftTracker.rawGoverned.push({
|
|
1556
3520
|
agent: r.stakeholder_id,
|
|
@@ -1571,6 +3535,7 @@ function trackShift(event) {
|
|
|
1571
3535
|
|
|
1572
3536
|
function renderSystemShift(result) {
|
|
1573
3537
|
if (shiftTracker.blocks === 0) return;
|
|
3538
|
+
if (!ssCard) return; // System shift card removed from visible UI
|
|
1574
3539
|
|
|
1575
3540
|
var adaptRate = shiftTracker.total > 0 ? Math.round((shiftTracker.blocks / shiftTracker.total) * 100) : 0;
|
|
1576
3541
|
|
|
@@ -1848,6 +3813,468 @@ handleEvent = function(event) {
|
|
|
1848
3813
|
}
|
|
1849
3814
|
};
|
|
1850
3815
|
|
|
3816
|
+
// ============================================
|
|
3817
|
+
// THEME TOGGLE — Light / Dark mode
|
|
3818
|
+
// ============================================
|
|
3819
|
+
const themeToggleBtn = document.getElementById('theme-toggle');
|
|
3820
|
+
function applyTheme(theme) {
|
|
3821
|
+
if (theme === 'light') {
|
|
3822
|
+
document.body.classList.add('light');
|
|
3823
|
+
themeToggleBtn.textContent = 'Dark Mode';
|
|
3824
|
+
} else {
|
|
3825
|
+
document.body.classList.remove('light');
|
|
3826
|
+
themeToggleBtn.textContent = 'Light Mode';
|
|
3827
|
+
}
|
|
3828
|
+
localStorage.setItem('nv-theme', theme);
|
|
3829
|
+
}
|
|
3830
|
+
themeToggleBtn.addEventListener('click', () => {
|
|
3831
|
+
const current = document.body.classList.contains('light') ? 'light' : 'dark';
|
|
3832
|
+
applyTheme(current === 'light' ? 'dark' : 'light');
|
|
3833
|
+
});
|
|
3834
|
+
// Restore saved theme
|
|
3835
|
+
const savedTheme = localStorage.getItem('nv-theme');
|
|
3836
|
+
if (savedTheme) applyTheme(savedTheme);
|
|
3837
|
+
|
|
3838
|
+
// ============================================
|
|
3839
|
+
// WORLD SOURCE SWITCHING
|
|
3840
|
+
// ============================================
|
|
3841
|
+
let currentWorldSource = 'preset';
|
|
3842
|
+
const worldSourceTabs = document.querySelectorAll('.ws-tab');
|
|
3843
|
+
const sourcePresetPanel = document.getElementById('source-preset');
|
|
3844
|
+
const sourceCustomPanel = document.getElementById('source-custom');
|
|
3845
|
+
const sourceUploadPanel = document.getElementById('source-upload');
|
|
3846
|
+
|
|
3847
|
+
worldSourceTabs.forEach(tab => {
|
|
3848
|
+
tab.addEventListener('click', () => {
|
|
3849
|
+
const source = tab.dataset.source;
|
|
3850
|
+
if (source === currentWorldSource) return;
|
|
3851
|
+
|
|
3852
|
+
currentWorldSource = source;
|
|
3853
|
+
|
|
3854
|
+
// Update tab visuals
|
|
3855
|
+
worldSourceTabs.forEach(t => t.classList.remove('active'));
|
|
3856
|
+
tab.classList.add('active');
|
|
3857
|
+
tab.querySelector('input').checked = true;
|
|
3858
|
+
|
|
3859
|
+
// Show/hide panels
|
|
3860
|
+
sourcePresetPanel.style.display = source === 'preset' ? '' : 'none';
|
|
3861
|
+
sourceCustomPanel.style.display = source === 'custom' ? '' : 'none';
|
|
3862
|
+
sourceUploadPanel.style.display = source === 'upload' ? '' : 'none';
|
|
3863
|
+
});
|
|
3864
|
+
});
|
|
3865
|
+
|
|
3866
|
+
// Populate base world selector in custom rules panel
|
|
3867
|
+
function populateBaseWorldSelect() {
|
|
3868
|
+
const select = document.getElementById('custom-base-world');
|
|
3869
|
+
if (!select) return;
|
|
3870
|
+
worlds.forEach(w => {
|
|
3871
|
+
const opt = document.createElement('option');
|
|
3872
|
+
opt.value = w.id;
|
|
3873
|
+
opt.textContent = w.title;
|
|
3874
|
+
select.appendChild(opt);
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
// ============================================
|
|
3879
|
+
// WORLD ACTION BAR
|
|
3880
|
+
// ============================================
|
|
3881
|
+
|
|
3882
|
+
// + New World
|
|
3883
|
+
document.getElementById('new-world-btn').addEventListener('click', () => {
|
|
3884
|
+
// Switch to custom rules mode
|
|
3885
|
+
currentWorldSource = 'custom';
|
|
3886
|
+
worldSourceTabs.forEach(t => {
|
|
3887
|
+
t.classList.toggle('active', t.dataset.source === 'custom');
|
|
3888
|
+
t.querySelector('input').checked = t.dataset.source === 'custom';
|
|
3889
|
+
});
|
|
3890
|
+
sourcePresetPanel.style.display = 'none';
|
|
3891
|
+
sourceCustomPanel.style.display = '';
|
|
3892
|
+
sourceUploadPanel.style.display = 'none';
|
|
3893
|
+
|
|
3894
|
+
// Clear everything
|
|
3895
|
+
document.getElementById('custom-world-name').value = '';
|
|
3896
|
+
document.getElementById('custom-world-thesis').value = '';
|
|
3897
|
+
document.getElementById('rule-input').value = '';
|
|
3898
|
+
document.getElementById('parsed-rules').innerHTML = '';
|
|
3899
|
+
document.getElementById('rule-status').textContent = '';
|
|
3900
|
+
document.getElementById('rule-status').className = 'rule-status';
|
|
3901
|
+
document.getElementById('custom-base-world').value = '';
|
|
3902
|
+
|
|
3903
|
+
// Clear active rules server-side
|
|
3904
|
+
fetch('/api/clear-rules', { method: 'POST' });
|
|
3905
|
+
|
|
3906
|
+
// Reset right panel
|
|
3907
|
+
document.getElementById('active-invariants').innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No rules loaded. Define your world.</div>';
|
|
3908
|
+
});
|
|
3909
|
+
|
|
3910
|
+
// Clear Rules
|
|
3911
|
+
document.getElementById('clear-rules-btn').addEventListener('click', async () => {
|
|
3912
|
+
await fetch('/api/clear-rules', { method: 'POST' });
|
|
3913
|
+
|
|
3914
|
+
// Clear rule editor UI
|
|
3915
|
+
const ruleInput = document.getElementById('rule-input');
|
|
3916
|
+
if (ruleInput) ruleInput.value = '';
|
|
3917
|
+
const parsed = document.getElementById('parsed-rules');
|
|
3918
|
+
if (parsed) parsed.innerHTML = '';
|
|
3919
|
+
const status = document.getElementById('rule-status');
|
|
3920
|
+
if (status) { status.textContent = 'Rules cleared.'; status.className = 'rule-status success'; }
|
|
3921
|
+
|
|
3922
|
+
// Clear upload state
|
|
3923
|
+
const uploadStatus = document.getElementById('upload-status');
|
|
3924
|
+
if (uploadStatus) { uploadStatus.textContent = 'Rules cleared.'; uploadStatus.className = 'rule-status success'; }
|
|
3925
|
+
const loadedInfo = document.getElementById('loaded-world-info');
|
|
3926
|
+
if (loadedInfo) loadedInfo.style.display = 'none';
|
|
3927
|
+
});
|
|
3928
|
+
|
|
3929
|
+
// Load World File (switch to upload tab)
|
|
3930
|
+
document.getElementById('load-file-btn').addEventListener('click', () => {
|
|
3931
|
+
currentWorldSource = 'upload';
|
|
3932
|
+
worldSourceTabs.forEach(t => {
|
|
3933
|
+
t.classList.toggle('active', t.dataset.source === 'upload');
|
|
3934
|
+
t.querySelector('input').checked = t.dataset.source === 'upload';
|
|
3935
|
+
});
|
|
3936
|
+
sourcePresetPanel.style.display = 'none';
|
|
3937
|
+
sourceCustomPanel.style.display = 'none';
|
|
3938
|
+
sourceUploadPanel.style.display = '';
|
|
3939
|
+
});
|
|
3940
|
+
|
|
3941
|
+
// Save as World File (export)
|
|
3942
|
+
document.getElementById('export-world-btn').addEventListener('click', async () => {
|
|
3943
|
+
try {
|
|
3944
|
+
const resp = await fetch('/api/export-world');
|
|
3945
|
+
const data = await resp.json();
|
|
3946
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
3947
|
+
const url = URL.createObjectURL(blob);
|
|
3948
|
+
const a = document.createElement('a');
|
|
3949
|
+
a.href = url;
|
|
3950
|
+
a.download = (currentWorld ? currentWorld.id : 'custom') + '-world.json';
|
|
3951
|
+
document.body.appendChild(a);
|
|
3952
|
+
a.click();
|
|
3953
|
+
document.body.removeChild(a);
|
|
3954
|
+
URL.revokeObjectURL(url);
|
|
3955
|
+
} catch (err) {
|
|
3956
|
+
alert('Export failed: ' + err.message);
|
|
3957
|
+
}
|
|
3958
|
+
});
|
|
3959
|
+
|
|
3960
|
+
// ============================================
|
|
3961
|
+
// WORLD FILE UPLOAD / PASTE
|
|
3962
|
+
// ============================================
|
|
3963
|
+
const uploadZone = document.getElementById('upload-zone');
|
|
3964
|
+
const uploadFileInput = document.getElementById('upload-file-input');
|
|
3965
|
+
const uploadBrowseBtn = document.getElementById('upload-browse-btn');
|
|
3966
|
+
const worldJsonInput = document.getElementById('world-json-input');
|
|
3967
|
+
const loadWorldBtn = document.getElementById('load-world-btn');
|
|
3968
|
+
const uploadStatusEl = document.getElementById('upload-status');
|
|
3969
|
+
|
|
3970
|
+
// Drag and drop
|
|
3971
|
+
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
|
|
3972
|
+
uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
|
|
3973
|
+
uploadZone.addEventListener('drop', (e) => {
|
|
3974
|
+
e.preventDefault();
|
|
3975
|
+
uploadZone.classList.remove('dragover');
|
|
3976
|
+
const file = e.dataTransfer.files[0];
|
|
3977
|
+
if (file) readWorldFile(file);
|
|
3978
|
+
});
|
|
3979
|
+
|
|
3980
|
+
// Browse button
|
|
3981
|
+
uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadFileInput.click(); });
|
|
3982
|
+
uploadFileInput.addEventListener('change', () => {
|
|
3983
|
+
if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]);
|
|
3984
|
+
});
|
|
3985
|
+
|
|
3986
|
+
// Click zone to browse
|
|
3987
|
+
uploadZone.addEventListener('click', () => { uploadFileInput.click(); });
|
|
3988
|
+
|
|
3989
|
+
function readWorldFile(file) {
|
|
3990
|
+
const reader = new FileReader();
|
|
3991
|
+
reader.onload = (e) => {
|
|
3992
|
+
worldJsonInput.value = e.target.result;
|
|
3993
|
+
uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".';
|
|
3994
|
+
uploadStatusEl.className = 'rule-status success';
|
|
3995
|
+
};
|
|
3996
|
+
reader.readAsText(file);
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
// Load into Runtime
|
|
4000
|
+
loadWorldBtn.addEventListener('click', async () => {
|
|
4001
|
+
const jsonText = worldJsonInput.value.trim();
|
|
4002
|
+
if (!jsonText) {
|
|
4003
|
+
uploadStatusEl.textContent = 'Paste or upload a world file first.';
|
|
4004
|
+
uploadStatusEl.className = 'rule-status error';
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
let worldData;
|
|
4009
|
+
try {
|
|
4010
|
+
worldData = JSON.parse(jsonText);
|
|
4011
|
+
} catch (err) {
|
|
4012
|
+
uploadStatusEl.textContent = 'Invalid JSON: ' + err.message;
|
|
4013
|
+
uploadStatusEl.className = 'rule-status error';
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
// Normalize: if the JSON is { world: {...} } or just {...}
|
|
4018
|
+
const worldPayload = worldData.world || worldData;
|
|
4019
|
+
|
|
4020
|
+
loadWorldBtn.textContent = 'Loading...';
|
|
4021
|
+
loadWorldBtn.disabled = true;
|
|
4022
|
+
|
|
4023
|
+
try {
|
|
4024
|
+
const resp = await fetch('/api/load-world-file', {
|
|
4025
|
+
method: 'POST',
|
|
4026
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4027
|
+
body: JSON.stringify({ world: worldPayload }),
|
|
4028
|
+
});
|
|
4029
|
+
const result = await resp.json();
|
|
4030
|
+
|
|
4031
|
+
if (result.error) {
|
|
4032
|
+
uploadStatusEl.textContent = result.error;
|
|
4033
|
+
uploadStatusEl.className = 'rule-status error';
|
|
4034
|
+
} else {
|
|
4035
|
+
uploadStatusEl.textContent = result.message;
|
|
4036
|
+
uploadStatusEl.className = 'rule-status success';
|
|
4037
|
+
|
|
4038
|
+
// Show loaded world info
|
|
4039
|
+
const infoEl = document.getElementById('loaded-world-info');
|
|
4040
|
+
infoEl.style.display = '';
|
|
4041
|
+
document.getElementById('lw-name').textContent = result.world.title;
|
|
4042
|
+
document.getElementById('lw-thesis').textContent = '"' + result.world.thesis + '"';
|
|
4043
|
+
document.getElementById('lw-stats').textContent =
|
|
4044
|
+
result.world.invariants.length + ' invariants, ' +
|
|
4045
|
+
result.world.gates.length + ' gates, ' +
|
|
4046
|
+
result.rulesApplied + ' rules';
|
|
4047
|
+
|
|
4048
|
+
// Update active invariants in right panel
|
|
4049
|
+
const invHtml = result.world.invariants.map(inv =>
|
|
4050
|
+
'<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
|
|
4051
|
+
).join('') + result.world.gates.map(g =>
|
|
4052
|
+
'<div class="inv-item" style="color:' + (g.severity === 'critical' ? 'var(--red)' : 'var(--yellow)') + '">[' + g.id + '] ' + g.label + '</div>'
|
|
4053
|
+
).join('');
|
|
4054
|
+
activeInvEl.innerHTML = invHtml || '<div style="font-size:11px;color:var(--text-muted)">No invariants defined</div>';
|
|
4055
|
+
|
|
4056
|
+
// Update state variables if present
|
|
4057
|
+
if (result.world.stateVariables && result.world.stateVariables.length > 0) {
|
|
4058
|
+
// Store as a pseudo-world so sliders render
|
|
4059
|
+
currentWorld = {
|
|
4060
|
+
id: 'custom-world',
|
|
4061
|
+
title: result.world.title,
|
|
4062
|
+
thesis: result.world.thesis,
|
|
4063
|
+
stateVariables: result.world.stateVariables,
|
|
4064
|
+
invariants: result.world.invariants,
|
|
4065
|
+
gates: result.world.gates,
|
|
4066
|
+
};
|
|
4067
|
+
selectWorld('custom-world');
|
|
4068
|
+
} else {
|
|
4069
|
+
// Just set current world reference
|
|
4070
|
+
currentWorld = {
|
|
4071
|
+
id: 'custom-world',
|
|
4072
|
+
title: result.world.title,
|
|
4073
|
+
thesis: result.world.thesis,
|
|
4074
|
+
stateVariables: [],
|
|
4075
|
+
invariants: result.world.invariants,
|
|
4076
|
+
gates: result.world.gates,
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
} catch (err) {
|
|
4081
|
+
uploadStatusEl.textContent = 'Error: ' + err.message;
|
|
4082
|
+
uploadStatusEl.className = 'rule-status error';
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
loadWorldBtn.textContent = 'Load into Runtime';
|
|
4086
|
+
loadWorldBtn.disabled = false;
|
|
4087
|
+
});
|
|
4088
|
+
|
|
4089
|
+
// ============================================
|
|
4090
|
+
// PLAIN-ENGLISH RULE EDITOR
|
|
4091
|
+
// ============================================
|
|
4092
|
+
const ruleInput = document.getElementById('rule-input');
|
|
4093
|
+
const parseRulesBtn = document.getElementById('parse-rules-btn');
|
|
4094
|
+
const parsedRulesEl = document.getElementById('parsed-rules');
|
|
4095
|
+
const ruleStatusEl = document.getElementById('rule-status');
|
|
4096
|
+
let parsedRuleData = [];
|
|
4097
|
+
|
|
4098
|
+
parseRulesBtn.addEventListener('click', async () => {
|
|
4099
|
+
const text = ruleInput.value.trim();
|
|
4100
|
+
if (!text) return;
|
|
4101
|
+
|
|
4102
|
+
parseRulesBtn.disabled = true;
|
|
4103
|
+
parseRulesBtn.textContent = 'Parsing...';
|
|
4104
|
+
ruleStatusEl.textContent = '';
|
|
4105
|
+
ruleStatusEl.className = 'rule-status';
|
|
4106
|
+
|
|
4107
|
+
try {
|
|
4108
|
+
const resp = await fetch('/api/parse-rules', {
|
|
4109
|
+
method: 'POST',
|
|
4110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4111
|
+
body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
|
|
4112
|
+
});
|
|
4113
|
+
const data = await resp.json();
|
|
4114
|
+
|
|
4115
|
+
if (data.error) {
|
|
4116
|
+
ruleStatusEl.textContent = data.error;
|
|
4117
|
+
ruleStatusEl.className = 'rule-status error';
|
|
4118
|
+
parsedRulesEl.innerHTML = '';
|
|
4119
|
+
parsedRuleData = [];
|
|
4120
|
+
} else {
|
|
4121
|
+
parsedRuleData = data.rules || [];
|
|
4122
|
+
parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
|
|
4123
|
+
const enfType = r.enforcement || 'block';
|
|
4124
|
+
const iconMap = { block: '🔴', allow: '🟢', modify: '🔵', warn: '🟡', pause: '🟡' };
|
|
4125
|
+
const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
|
|
4126
|
+
const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
|
|
4127
|
+
const icon = iconMap[enfType] || '🟢';
|
|
4128
|
+
const label = labelMap[enfType] || 'Rule';
|
|
4129
|
+
const effect = effectMap[enfType] || 'Active';
|
|
4130
|
+
return '<div class="parsed-rule enforcement-' + enfType + '">' +
|
|
4131
|
+
'<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
|
|
4132
|
+
'<div class="pr-desc">' + r.description + '</div>' +
|
|
4133
|
+
'<div class="pr-patterns">' + effect + ' • Matches: ' + r.intent_patterns.join(', ') + '</div>' +
|
|
4134
|
+
'</div>';
|
|
4135
|
+
}).join('');
|
|
4136
|
+
|
|
4137
|
+
if (parsedRuleData.length > 0) {
|
|
4138
|
+
const btnLabel = currentWorldSource === 'custom' ? 'Generate World with ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') : 'Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation';
|
|
4139
|
+
parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">' + btnLabel + '</button>';
|
|
4140
|
+
document.getElementById('apply-rules-btn').addEventListener('click', async () => {
|
|
4141
|
+
try {
|
|
4142
|
+
// If in custom rules mode, use base world if selected
|
|
4143
|
+
let worldId = currentWorld ? currentWorld.id : 'trading';
|
|
4144
|
+
if (currentWorldSource === 'custom') {
|
|
4145
|
+
const baseWorld = document.getElementById('custom-base-world').value;
|
|
4146
|
+
if (baseWorld) worldId = baseWorld;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
const applyResp = await fetch('/api/apply-rules', {
|
|
4150
|
+
method: 'POST',
|
|
4151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4152
|
+
body: JSON.stringify({ rules: parsedRuleData, worldId }),
|
|
4153
|
+
});
|
|
4154
|
+
const applyData = await applyResp.json();
|
|
4155
|
+
if (applyData.status === 'applied') {
|
|
4156
|
+
ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
|
|
4157
|
+
ruleStatusEl.className = 'rule-status success';
|
|
4158
|
+
|
|
4159
|
+
// Update right panel invariants with custom rules
|
|
4160
|
+
const customName = document.getElementById('custom-world-name');
|
|
4161
|
+
const worldName = (customName && customName.value) ? customName.value : 'Custom World';
|
|
4162
|
+
const customThesis = document.getElementById('custom-world-thesis');
|
|
4163
|
+
const thesis = (customThesis && customThesis.value) ? customThesis.value : 'User-defined governance rules';
|
|
4164
|
+
|
|
4165
|
+
// Show rules in active invariants panel
|
|
4166
|
+
activeInvEl.innerHTML = parsedRuleData.map(r => {
|
|
4167
|
+
const enfType = r.enforcement || 'block';
|
|
4168
|
+
const colorMap = { block: 'var(--red)', allow: 'var(--green)', modify: 'var(--blue)', warn: 'var(--yellow)', pause: 'var(--yellow)' };
|
|
4169
|
+
const color = colorMap[enfType] || 'var(--text-secondary)';
|
|
4170
|
+
return '<div class="inv-item" style="color:' + color + '">[' + r.id + '] ' + r.description + '</div>';
|
|
4171
|
+
}).join('');
|
|
4172
|
+
|
|
4173
|
+
// In custom mode, set a custom world reference
|
|
4174
|
+
if (currentWorldSource === 'custom') {
|
|
4175
|
+
const baseWorld = document.getElementById('custom-base-world').value;
|
|
4176
|
+
if (baseWorld) {
|
|
4177
|
+
selectWorld(baseWorld);
|
|
4178
|
+
} else {
|
|
4179
|
+
currentWorld = { id: 'custom-world', title: worldName, thesis, stateVariables: [], invariants: [], gates: [] };
|
|
4180
|
+
}
|
|
4181
|
+
document.getElementById('world-thesis').textContent = '"' + thesis + '"';
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
} catch (err) {
|
|
4185
|
+
ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
|
|
4186
|
+
ruleStatusEl.className = 'rule-status error';
|
|
4187
|
+
}
|
|
4188
|
+
});
|
|
4189
|
+
ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
|
|
4190
|
+
ruleStatusEl.className = 'rule-status success';
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
} catch (err) {
|
|
4194
|
+
ruleStatusEl.textContent = 'Error: ' + err.message;
|
|
4195
|
+
ruleStatusEl.className = 'rule-status error';
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
parseRulesBtn.disabled = false;
|
|
4199
|
+
parseRulesBtn.textContent = 'Parse Rules';
|
|
4200
|
+
});
|
|
4201
|
+
|
|
4202
|
+
// ============================================
|
|
4203
|
+
// SESSION TRACKING
|
|
4204
|
+
// ============================================
|
|
4205
|
+
|
|
4206
|
+
let sessionPollInterval = null;
|
|
4207
|
+
|
|
4208
|
+
async function pollSessionStats() {
|
|
4209
|
+
try {
|
|
4210
|
+
const resp = await fetch('/api/session');
|
|
4211
|
+
const data = await resp.json();
|
|
4212
|
+
const el = (id) => document.getElementById(id);
|
|
4213
|
+
if (el('s-total')) el('s-total').textContent = data.evaluations.total;
|
|
4214
|
+
if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
|
|
4215
|
+
if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
|
|
4216
|
+
if (el('s-allowed')) el('s-allowed').textContent = data.evaluations.allowed;
|
|
4217
|
+
if (el('s-agents')) {
|
|
4218
|
+
el('s-agents').textContent = data.agents.length > 0
|
|
4219
|
+
? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
|
|
4220
|
+
: 'No agents connected yet';
|
|
4221
|
+
}
|
|
4222
|
+
if (el('session-history') && data.historyCount > 0) {
|
|
4223
|
+
el('session-history').textContent = data.historyCount + ' previous session(s) saved for comparison';
|
|
4224
|
+
}
|
|
4225
|
+
} catch {}
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
async function viewSessionReport() {
|
|
4229
|
+
try {
|
|
4230
|
+
const resp = await fetch('/api/session/report');
|
|
4231
|
+
const text = await resp.text();
|
|
4232
|
+
const log = document.getElementById('sim-log');
|
|
4233
|
+
if (log) {
|
|
4234
|
+
const div = document.createElement('div');
|
|
4235
|
+
div.className = 'log-round';
|
|
4236
|
+
div.innerHTML = '<h4 style="color:#818cf8">Enforcement Report</h4><pre style="white-space:pre-wrap;font-size:11px;color:#d4d4d8">' + text.replace(/</g,'<') + '</pre>';
|
|
4237
|
+
log.prepend(div);
|
|
4238
|
+
log.scrollTop = 0;
|
|
4239
|
+
}
|
|
4240
|
+
} catch (err) { console.error('Failed to load report', err); }
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
async function resetSession() {
|
|
4244
|
+
if (!confirm('Reset session? Current data will be saved for comparison.')) return;
|
|
4245
|
+
try {
|
|
4246
|
+
const resp = await fetch('/api/session/reset', { method: 'POST' });
|
|
4247
|
+
const data = await resp.json();
|
|
4248
|
+
const log = document.getElementById('sim-log');
|
|
4249
|
+
if (log) {
|
|
4250
|
+
const div = document.createElement('div');
|
|
4251
|
+
div.className = 'log-round';
|
|
4252
|
+
div.innerHTML = '<h4 style="color:#fbbf24">Session Reset</h4><div style="font-size:11px;color:#a8a29e">' + data.message + '</div>';
|
|
4253
|
+
log.prepend(div);
|
|
4254
|
+
}
|
|
4255
|
+
pollSessionStats();
|
|
4256
|
+
} catch (err) { console.error('Failed to reset session', err); }
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
async function saveExperiment() {
|
|
4260
|
+
try {
|
|
4261
|
+
const resp = await fetch('/api/session/save', { method: 'POST' });
|
|
4262
|
+
const data = await resp.json();
|
|
4263
|
+
if (data.error) { alert(data.error); return; }
|
|
4264
|
+
const log = document.getElementById('sim-log');
|
|
4265
|
+
if (log) {
|
|
4266
|
+
const div = document.createElement('div');
|
|
4267
|
+
div.className = 'log-round';
|
|
4268
|
+
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>';
|
|
4269
|
+
log.prepend(div);
|
|
4270
|
+
}
|
|
4271
|
+
} catch (err) { console.error('Failed to save experiment', err); }
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
// Poll session stats every 2 seconds
|
|
4275
|
+
sessionPollInterval = setInterval(pollSessionStats, 2000);
|
|
4276
|
+
pollSessionStats();
|
|
4277
|
+
|
|
1851
4278
|
// ============================================
|
|
1852
4279
|
// BOOT
|
|
1853
4280
|
// ============================================
|