@neuroverseos/nv-sim 0.1.7 → 0.1.10
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 +375 -197
- package/connectors/nv_mirofish_wrapper.py +841 -0
- package/connectors/nv_scienceclaw_wrapper.py +453 -0
- package/dist/adapters/scienceclaw.js +52 -2
- package/dist/assets/index-B43_0HyO.css +1 -0
- package/dist/assets/index-CdghpsS8.js +595 -0
- package/dist/assets/{reportEngine-BVdQ2_nW.js → reportEngine-CYSZfooa.js} +1 -1
- package/dist/connectors/nv-scienceclaw-post.js +376 -0
- package/dist/engine/aiProvider.js +82 -3
- package/dist/engine/analyzer.js +12 -24
- package/dist/engine/chaosEngine.js +3 -9
- package/dist/engine/cli.js +123 -218
- package/dist/engine/dynamicsGovernance.js +4 -0
- package/dist/engine/fullGovernedLoop.js +16 -1
- package/dist/engine/goalEngine.js +3 -4
- package/dist/engine/governance.js +18 -0
- package/dist/engine/index.js +19 -29
- package/dist/engine/intentTranslator.js +281 -0
- package/dist/engine/liveAdapter.js +100 -18
- package/dist/engine/liveVisualizer.js +2656 -866
- package/dist/engine/narrativeInjection.js +78 -89
- package/dist/engine/policyEngine.js +171 -58
- package/dist/engine/primeRadiant.js +2 -8
- package/dist/engine/reasoningEngine.js +2 -7
- package/dist/engine/scenarioCapsule.js +77 -133
- package/dist/engine/scenarioLibrary.js +52 -131
- package/dist/engine/swarmSimulation.js +1 -9
- package/dist/engine/worldBridge.js +22 -8
- package/dist/engine/worldComparison.js +12 -25
- package/dist/index.html +2 -2
- package/dist/lib/reasoningEngine.js +17 -1
- package/dist/lib/simulationAdapter.js +11 -11
- package/dist/lib/swarmParser.js +1 -1
- package/dist/runtime/govern.js +160 -7
- package/dist/runtime/index.js +1 -4
- package/dist/runtime/types.js +91 -0
- package/package.json +23 -6
- package/dist/adapters/mirofish.js +0 -461
- package/dist/assets/index-CHmUN8s0.js +0 -532
- package/dist/assets/index-DWgMnB7I.css +0 -1
- package/dist/assets/mirotir-logo-DUexumBH.svg +0 -185
- package/dist/engine/mirofish.js +0 -295
|
@@ -46,6 +46,7 @@ exports.startInteractiveServer = startInteractiveServer;
|
|
|
46
46
|
const http = __importStar(require("http"));
|
|
47
47
|
const fs = __importStar(require("fs"));
|
|
48
48
|
const path = __importStar(require("path"));
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
49
50
|
const swarmSimulation_1 = require("./swarmSimulation");
|
|
50
51
|
const worldBridge_1 = require("./worldBridge");
|
|
51
52
|
const auditTrace_1 = require("./auditTrace");
|
|
@@ -180,12 +181,96 @@ function startInteractiveServer(port, onReady) {
|
|
|
180
181
|
let currentSession = {
|
|
181
182
|
id: `session_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
182
183
|
startedAt: new Date().toISOString(),
|
|
183
|
-
world: "
|
|
184
|
+
world: "social_simulation",
|
|
184
185
|
guardCount: 0,
|
|
185
186
|
evaluations: [],
|
|
186
187
|
};
|
|
187
188
|
// Session snapshots for multi-run comparison
|
|
188
189
|
const sessionHistory = [];
|
|
190
|
+
// ── AI API Key (BYOK — Bring Your Own Key) ──
|
|
191
|
+
// Users configure their AI provider key through the dashboard.
|
|
192
|
+
// Stored in memory for the session; can also be set via environment variables.
|
|
193
|
+
let userAIConfig = {
|
|
194
|
+
provider: (process.env.ANTHROPIC_API_KEY ? "anthropic" : process.env.OPENAI_API_KEY ? "openai" : "anthropic"),
|
|
195
|
+
apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? "",
|
|
196
|
+
baseUrl: process.env.OPENAI_BASE_URL,
|
|
197
|
+
model: process.env.OPENAI_MODEL,
|
|
198
|
+
};
|
|
199
|
+
// ── Active Bridge Process ──
|
|
200
|
+
// Tracks a spawned simulator (e.g., ScienceClaw) launched from the visualizer
|
|
201
|
+
let activeBridgeProcess = null;
|
|
202
|
+
// ── Bridge Metrics Tracker ──
|
|
203
|
+
// Computes meaningful live metrics from external /api/evaluate calls
|
|
204
|
+
let bridgeEvalCount = 0;
|
|
205
|
+
let bridgeBlockCount = 0;
|
|
206
|
+
let bridgeModifyCount = 0;
|
|
207
|
+
let bridgeRewardCount = 0;
|
|
208
|
+
let bridgePenalizeCount = 0;
|
|
209
|
+
function computeBridgeMetrics(decision) {
|
|
210
|
+
bridgeEvalCount++;
|
|
211
|
+
if (decision === "BLOCK")
|
|
212
|
+
bridgeBlockCount++;
|
|
213
|
+
if (decision === "MODIFY")
|
|
214
|
+
bridgeModifyCount++;
|
|
215
|
+
if (decision === "REWARD")
|
|
216
|
+
bridgeRewardCount++;
|
|
217
|
+
if (decision === "PENALIZE")
|
|
218
|
+
bridgePenalizeCount++;
|
|
219
|
+
const totalInterventions = bridgeBlockCount + bridgeModifyCount + bridgePenalizeCount;
|
|
220
|
+
// Stability = ratio of non-blocked actions (higher = more stable)
|
|
221
|
+
const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount - bridgePenalizeCount) / bridgeEvalCount : 1;
|
|
222
|
+
// Volatility = ratio of interventions (blocks + modifies + penalizes) — more interventions = more volatile
|
|
223
|
+
const volatility = bridgeEvalCount > 0 ? totalInterventions / bridgeEvalCount : 0;
|
|
224
|
+
return { stability, volatility, totalInterventions, evalCount: bridgeEvalCount };
|
|
225
|
+
}
|
|
226
|
+
// ── Agent Behavior State Registry ──
|
|
227
|
+
// Tracks per-agent incentive state: cooldowns, influence, rewards/penalties
|
|
228
|
+
// Uses @neuroverseos/governance decision-flow-engine when available, falls back to local tracking
|
|
229
|
+
const agentBehaviorStates = new Map();
|
|
230
|
+
function getOrCreateAgentState(agentId) {
|
|
231
|
+
if (!agentBehaviorStates.has(agentId)) {
|
|
232
|
+
agentBehaviorStates.set(agentId, {
|
|
233
|
+
agentId,
|
|
234
|
+
cooldownRemaining: 0,
|
|
235
|
+
influence: 1.0,
|
|
236
|
+
rewardMultiplier: 1.0,
|
|
237
|
+
totalPenalties: 0,
|
|
238
|
+
totalRewards: 0,
|
|
239
|
+
consequenceHistory: [],
|
|
240
|
+
rewardHistory: [],
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return agentBehaviorStates.get(agentId);
|
|
244
|
+
}
|
|
245
|
+
function localApplyConsequence(agentId, consequence, ruleId) {
|
|
246
|
+
const state = getOrCreateAgentState(agentId);
|
|
247
|
+
state.totalPenalties++;
|
|
248
|
+
state.consequenceHistory.push({ ruleId, consequence, appliedAt: Date.now() });
|
|
249
|
+
if (consequence.type === "freeze" || consequence.type === "cooldown") {
|
|
250
|
+
state.cooldownRemaining = Math.max(state.cooldownRemaining, consequence.rounds ?? 1);
|
|
251
|
+
}
|
|
252
|
+
if (consequence.type === "reduce_influence") {
|
|
253
|
+
state.influence = Math.max(0, state.influence - (consequence.magnitude ?? 0.2));
|
|
254
|
+
}
|
|
255
|
+
return state;
|
|
256
|
+
}
|
|
257
|
+
function localApplyReward(agentId, reward, ruleId) {
|
|
258
|
+
const state = getOrCreateAgentState(agentId);
|
|
259
|
+
state.totalRewards++;
|
|
260
|
+
state.rewardHistory.push({ ruleId, reward, appliedAt: Date.now() });
|
|
261
|
+
if (reward.type === "boost_influence") {
|
|
262
|
+
state.influence = Math.min(2.0, state.influence + (reward.magnitude ?? 0.1));
|
|
263
|
+
}
|
|
264
|
+
if (reward.type === "weight_increase") {
|
|
265
|
+
state.rewardMultiplier = Math.min(3.0, state.rewardMultiplier + (reward.magnitude ?? 0.1));
|
|
266
|
+
}
|
|
267
|
+
return state;
|
|
268
|
+
}
|
|
269
|
+
function localTickAgentStates() {
|
|
270
|
+
for (const [, state] of agentBehaviorStates) {
|
|
271
|
+
state.cooldownRemaining = Math.max(0, state.cooldownRemaining - 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
189
274
|
function synthesizeSessionReport() {
|
|
190
275
|
// Gather all sessions to report on (history + current if it has data)
|
|
191
276
|
const allSessions = [
|
|
@@ -499,7 +584,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
499
584
|
}
|
|
500
585
|
if (req.url === "/api/worlds" && req.method === "GET") {
|
|
501
586
|
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
502
|
-
const worldIds = ["
|
|
587
|
+
const worldIds = ["social_simulation", "science_research"];
|
|
503
588
|
const worlds = worldIds.map(id => {
|
|
504
589
|
try {
|
|
505
590
|
const r = resolveWorld(id);
|
|
@@ -536,24 +621,47 @@ function startInteractiveServer(port, onReady) {
|
|
|
536
621
|
try {
|
|
537
622
|
const body = await readBody(req);
|
|
538
623
|
const payload = JSON.parse(body);
|
|
539
|
-
if (!payload.name
|
|
540
|
-
jsonResponse(res, 400, { error: "name
|
|
624
|
+
if (!payload.name) {
|
|
625
|
+
jsonResponse(res, 400, { error: "name is required" });
|
|
541
626
|
return;
|
|
542
627
|
}
|
|
543
628
|
const id = slugify(payload.name);
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
629
|
+
// Support both old WorldVariant format and new SimulationRules format
|
|
630
|
+
if (payload.thesis !== undefined) {
|
|
631
|
+
// New SimulationRules format — complete snapshot
|
|
632
|
+
const rules = {
|
|
633
|
+
id,
|
|
634
|
+
name: payload.name,
|
|
635
|
+
description: payload.description ?? "",
|
|
636
|
+
thesis: payload.thesis,
|
|
637
|
+
bridgeId: payload.bridgeId,
|
|
638
|
+
rules: payload.rules ?? [],
|
|
639
|
+
worldDefinition: payload.worldDefinition,
|
|
640
|
+
stateOverrides: payload.stateOverrides ?? {},
|
|
641
|
+
createdAt: new Date().toISOString(),
|
|
642
|
+
lastResult: payload.lastResult,
|
|
643
|
+
};
|
|
644
|
+
ensureVariantsDir();
|
|
645
|
+
const filepath = path.join(getVariantsDir(), `${id}.json`);
|
|
646
|
+
fs.writeFileSync(filepath, JSON.stringify(rules, null, 2), "utf-8");
|
|
647
|
+
jsonResponse(res, 200, { status: "saved", variant: rules, filepath });
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
// Legacy WorldVariant format
|
|
651
|
+
const variant = {
|
|
652
|
+
id,
|
|
653
|
+
name: payload.name,
|
|
654
|
+
description: payload.description ?? "",
|
|
655
|
+
baseWorld: payload.baseWorld ?? "unknown",
|
|
656
|
+
stateOverrides: payload.stateOverrides ?? {},
|
|
657
|
+
events: payload.events ?? [],
|
|
658
|
+
rounds: payload.rounds ?? 5,
|
|
659
|
+
createdAt: new Date().toISOString(),
|
|
660
|
+
lastResult: payload.lastResult,
|
|
661
|
+
};
|
|
662
|
+
const filepath = saveVariant(variant);
|
|
663
|
+
jsonResponse(res, 200, { status: "saved", variant, filepath });
|
|
664
|
+
}
|
|
557
665
|
}
|
|
558
666
|
catch (err) {
|
|
559
667
|
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
@@ -602,7 +710,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
602
710
|
return;
|
|
603
711
|
}
|
|
604
712
|
// Resolve world for evaluation
|
|
605
|
-
const worldId = payload.world ?? "
|
|
713
|
+
const worldId = payload.world ?? "social_simulation";
|
|
606
714
|
let nvWorld;
|
|
607
715
|
try {
|
|
608
716
|
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
@@ -671,7 +779,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
671
779
|
default_enabled: true,
|
|
672
780
|
},
|
|
673
781
|
];
|
|
674
|
-
// Social media guards for
|
|
782
|
+
// Social media guards for OASIS agent actions.
|
|
675
783
|
// These govern what AI agents can do on simulated social platforms.
|
|
676
784
|
const socialGuards = [
|
|
677
785
|
{
|
|
@@ -741,6 +849,8 @@ function startInteractiveServer(port, onReady) {
|
|
|
741
849
|
immutable: false,
|
|
742
850
|
intent_patterns: cg.intent_patterns,
|
|
743
851
|
default_enabled: true,
|
|
852
|
+
...(cg.consequence ? { consequence: cg.consequence } : {}),
|
|
853
|
+
...(cg.reward ? { reward: cg.reward } : {}),
|
|
744
854
|
});
|
|
745
855
|
// Add patterns to vocabulary
|
|
746
856
|
for (const pat of cg.intent_patterns) {
|
|
@@ -798,6 +908,90 @@ function startInteractiveServer(port, onReady) {
|
|
|
798
908
|
});
|
|
799
909
|
return;
|
|
800
910
|
}
|
|
911
|
+
// ── Local incentive guard check ──
|
|
912
|
+
// Custom guards with enforcement=penalize or enforcement=reward need local matching
|
|
913
|
+
// because the fallback evaluateScenarioGuard only knows ALLOW/BLOCK/PAUSE.
|
|
914
|
+
// Also check state-based conditions (e.g. low confidence → penalize publishing).
|
|
915
|
+
const actionLower = payload.action.toLowerCase().replace(/_/g, " ");
|
|
916
|
+
const stateFacts = payload.state ?? {};
|
|
917
|
+
for (const cg of customGuards) {
|
|
918
|
+
if (cg.enforcement !== "penalize" && cg.enforcement !== "reward")
|
|
919
|
+
continue;
|
|
920
|
+
// Check if action matches any intent pattern
|
|
921
|
+
const matched = cg.intent_patterns.some(pat => {
|
|
922
|
+
const p = pat.toLowerCase();
|
|
923
|
+
return actionLower.includes(p) || p.includes(actionLower) || payload.action.toLowerCase() === p;
|
|
924
|
+
});
|
|
925
|
+
if (!matched)
|
|
926
|
+
continue;
|
|
927
|
+
// For penalize: check if state conditions indicate violation
|
|
928
|
+
// For reward: check if state conditions indicate good behavior
|
|
929
|
+
const isViolation = cg.enforcement === "penalize";
|
|
930
|
+
const isReward = cg.enforcement === "reward";
|
|
931
|
+
if (isViolation) {
|
|
932
|
+
const consequence = cg.consequence ?? { type: "freeze", rounds: 1, description: cg.description };
|
|
933
|
+
const v = {
|
|
934
|
+
status: "PENALIZE",
|
|
935
|
+
reason: cg.description,
|
|
936
|
+
ruleId: cg.id,
|
|
937
|
+
consequence: consequence,
|
|
938
|
+
};
|
|
939
|
+
// Record and respond
|
|
940
|
+
const agState = getOrCreateAgentState(payload.actor);
|
|
941
|
+
localApplyConsequence(payload.actor, consequence, cg.id);
|
|
942
|
+
const metrics = computeBridgeMetrics("PENALIZE");
|
|
943
|
+
broadcast({
|
|
944
|
+
type: "round", round: metrics.evalCount, totalRounds: 0, phase: "governed",
|
|
945
|
+
reactions: [{
|
|
946
|
+
stakeholder_id: payload.actor, reaction: payload.action, impact: -0.6, confidence: 0.5,
|
|
947
|
+
trigger: "bridge",
|
|
948
|
+
verdict: { status: "PENALIZE", reason: v.reason, ruleId: cg.id, incentive: { type: "penalize", magnitude: consequence.magnitude ?? 1, cooldownRounds: consequence.rounds ?? 0, description: consequence.description }, agentState: { cooldown: agState.cooldownRemaining, influence: agState.influence, penalties: agState.totalPenalties, rewards: agState.totalRewards } },
|
|
949
|
+
}],
|
|
950
|
+
avgImpact: -0.6, maxVolatility: metrics.volatility, dynamics: [], interventionCount: 1,
|
|
951
|
+
});
|
|
952
|
+
currentSession.evaluations.push({
|
|
953
|
+
actor: payload.actor, action: payload.action, decision: "PENALIZE",
|
|
954
|
+
reason: cg.description, ruleId: cg.id, world: payload.world ?? currentSession.world,
|
|
955
|
+
timestamp: Date.now(), payload: payload.payload,
|
|
956
|
+
incentive: { type: "penalize", magnitude: consequence.magnitude ?? 1, cooldownRounds: consequence.rounds ?? 0, description: consequence.description },
|
|
957
|
+
});
|
|
958
|
+
jsonResponse(res, 200, {
|
|
959
|
+
decision: "PENALIZE", reason: cg.description, rule_id: cg.id,
|
|
960
|
+
evidence: null, modified_action: null,
|
|
961
|
+
consequence, reward: null,
|
|
962
|
+
agent_state: { cooldown_remaining: agState.cooldownRemaining, influence: agState.influence, reward_multiplier: agState.rewardMultiplier, total_penalties: agState.totalPenalties, total_rewards: agState.totalRewards },
|
|
963
|
+
});
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (isReward) {
|
|
967
|
+
const reward = cg.reward ?? { type: "boost_influence", magnitude: 0.1, description: cg.description };
|
|
968
|
+
const agState = getOrCreateAgentState(payload.actor);
|
|
969
|
+
localApplyReward(payload.actor, reward, cg.id);
|
|
970
|
+
const metrics = computeBridgeMetrics("REWARD");
|
|
971
|
+
broadcast({
|
|
972
|
+
type: "round", round: metrics.evalCount, totalRounds: 0, phase: "governed",
|
|
973
|
+
reactions: [{
|
|
974
|
+
stakeholder_id: payload.actor, reaction: payload.action, impact: 0.5, confidence: 0.5,
|
|
975
|
+
trigger: "bridge",
|
|
976
|
+
verdict: { status: "REWARD", reason: cg.description, ruleId: cg.id, incentive: { type: "reward", magnitude: reward.magnitude ?? 0.1, cooldownRounds: 0, description: reward.description }, agentState: { cooldown: agState.cooldownRemaining, influence: agState.influence, penalties: agState.totalPenalties, rewards: agState.totalRewards } },
|
|
977
|
+
}],
|
|
978
|
+
avgImpact: 0.5, maxVolatility: metrics.volatility, dynamics: [], interventionCount: 0,
|
|
979
|
+
});
|
|
980
|
+
currentSession.evaluations.push({
|
|
981
|
+
actor: payload.actor, action: payload.action, decision: "REWARD",
|
|
982
|
+
reason: cg.description, ruleId: cg.id, world: payload.world ?? currentSession.world,
|
|
983
|
+
timestamp: Date.now(), payload: payload.payload,
|
|
984
|
+
incentive: { type: "reward", magnitude: reward.magnitude ?? 0.1, cooldownRounds: 0, description: reward.description },
|
|
985
|
+
});
|
|
986
|
+
jsonResponse(res, 200, {
|
|
987
|
+
decision: "REWARD", reason: cg.description, rule_id: cg.id,
|
|
988
|
+
evidence: null, modified_action: null,
|
|
989
|
+
consequence: null, reward,
|
|
990
|
+
agent_state: { cooldown_remaining: agState.cooldownRemaining, influence: agState.influence, reward_multiplier: agState.rewardMultiplier, total_penalties: agState.totalPenalties, total_rewards: agState.totalRewards },
|
|
991
|
+
});
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
801
995
|
// Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
|
|
802
996
|
// Omit `direction` — setting it enables execution-intent safety checks (prompt injection
|
|
803
997
|
// detection) which falsely flag financial terms like "buy" and "sell". Bridge actions are
|
|
@@ -812,6 +1006,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
812
1006
|
actor: payload.actor,
|
|
813
1007
|
action: payload.action,
|
|
814
1008
|
...(payload.payload ?? {}),
|
|
1009
|
+
...(payload.state ? { state: payload.state } : {}),
|
|
815
1010
|
},
|
|
816
1011
|
};
|
|
817
1012
|
// Evaluate directly via the governance module
|
|
@@ -830,36 +1025,116 @@ function startInteractiveServer(port, onReady) {
|
|
|
830
1025
|
stakeholders: [{ id: payload.actor, description: payload.actor, disposition: "neutral", priorities: [] }],
|
|
831
1026
|
}, nvWorld, { trace: true, level: "standard" });
|
|
832
1027
|
}
|
|
833
|
-
// Map verdict to bridge protocol
|
|
1028
|
+
// Map verdict to bridge protocol — includes REWARD/PENALIZE from governance engine
|
|
834
1029
|
const decision = verdict.status === "BLOCK" ? "BLOCK"
|
|
835
1030
|
: verdict.status === "PAUSE" ? "MODIFY"
|
|
836
|
-
: "
|
|
1031
|
+
: verdict.status === "REWARD" ? "REWARD"
|
|
1032
|
+
: verdict.status === "PENALIZE" ? "PENALIZE"
|
|
1033
|
+
: "ALLOW";
|
|
1034
|
+
// ── Apply incentive effects to agent behavior state ──
|
|
1035
|
+
// Uses @neuroverseos/governance decision-flow-engine types (consequence/reward)
|
|
1036
|
+
let agentState = getOrCreateAgentState(payload.actor);
|
|
1037
|
+
// Check if agent is in cooldown — if so, block regardless of verdict
|
|
1038
|
+
let effectiveDecision = decision;
|
|
1039
|
+
if (agentState.cooldownRemaining > 0 && decision !== "REWARD") {
|
|
1040
|
+
effectiveDecision = "PENALIZE";
|
|
1041
|
+
// Override verdict reason to explain cooldown
|
|
1042
|
+
verdict.reason = `Agent frozen: ${agentState.cooldownRemaining} round(s) remaining on cooldown`;
|
|
1043
|
+
}
|
|
1044
|
+
// Apply consequence (PENALIZE verdict)
|
|
1045
|
+
if (verdict.status === "PENALIZE" && verdict.consequence) {
|
|
1046
|
+
try {
|
|
1047
|
+
const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
|
|
1048
|
+
if (nv.applyConsequence) {
|
|
1049
|
+
agentState = nv.applyConsequence(agentState, verdict.consequence, verdict.ruleId ?? "unknown");
|
|
1050
|
+
agentBehaviorStates.set(payload.actor, agentState);
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
localApplyConsequence(payload.actor, verdict.consequence, verdict.ruleId ?? "unknown");
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
catch {
|
|
1057
|
+
localApplyConsequence(payload.actor, verdict.consequence, verdict.ruleId ?? "unknown");
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
else if (verdict.status === "PENALIZE") {
|
|
1061
|
+
// Fallback: no consequence payload, apply default freeze
|
|
1062
|
+
localApplyConsequence(payload.actor, { type: "freeze", rounds: 1, description: verdict.reason ?? "Penalized" }, verdict.ruleId ?? "unknown");
|
|
1063
|
+
}
|
|
1064
|
+
// Apply reward (REWARD verdict)
|
|
1065
|
+
if (verdict.status === "REWARD" && verdict.reward) {
|
|
1066
|
+
try {
|
|
1067
|
+
const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
|
|
1068
|
+
if (nv.applyReward) {
|
|
1069
|
+
agentState = nv.applyReward(agentState, verdict.reward, verdict.ruleId ?? "unknown");
|
|
1070
|
+
agentBehaviorStates.set(payload.actor, agentState);
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
localApplyReward(payload.actor, verdict.reward, verdict.ruleId ?? "unknown");
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
localApplyReward(payload.actor, verdict.reward, verdict.ruleId ?? "unknown");
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
else if (verdict.status === "REWARD") {
|
|
1081
|
+
// Fallback: no reward payload, apply default boost
|
|
1082
|
+
localApplyReward(payload.actor, { type: "boost_influence", magnitude: 0.1, description: verdict.reason ?? "Rewarded" }, verdict.ruleId ?? "unknown");
|
|
1083
|
+
}
|
|
1084
|
+
// Build incentive metadata for response (normalized from consequence/reward)
|
|
1085
|
+
const incentive = verdict.consequence
|
|
1086
|
+
? { type: "penalize", magnitude: verdict.consequence.magnitude ?? 1, cooldownRounds: verdict.consequence.rounds ?? 0, description: verdict.consequence.description }
|
|
1087
|
+
: verdict.reward
|
|
1088
|
+
? { type: "reward", magnitude: verdict.reward.magnitude ?? 1, cooldownRounds: 0, description: verdict.reward.description }
|
|
1089
|
+
: undefined;
|
|
1090
|
+
// Compute live metrics from cumulative bridge evaluations
|
|
1091
|
+
const bridgeMetrics = computeBridgeMetrics(effectiveDecision);
|
|
1092
|
+
// Impact scoring — factor in agent influence level
|
|
1093
|
+
const baseImpact = effectiveDecision === "BLOCK" ? -0.8
|
|
1094
|
+
: effectiveDecision === "PENALIZE" ? -0.6
|
|
1095
|
+
: effectiveDecision === "MODIFY" ? -0.3
|
|
1096
|
+
: effectiveDecision === "REWARD" ? 0.5
|
|
1097
|
+
: 0.1;
|
|
1098
|
+
const impactForDecision = baseImpact * agentState.influence;
|
|
837
1099
|
// Broadcast governance event to connected SSE clients
|
|
838
1100
|
broadcast({
|
|
839
1101
|
type: "round",
|
|
840
|
-
round:
|
|
1102
|
+
round: bridgeMetrics.evalCount,
|
|
841
1103
|
totalRounds: 0,
|
|
842
1104
|
phase: "governed",
|
|
843
1105
|
reactions: [{
|
|
844
1106
|
stakeholder_id: payload.actor,
|
|
845
1107
|
reaction: payload.action,
|
|
846
|
-
impact:
|
|
847
|
-
confidence: 0.5,
|
|
1108
|
+
impact: impactForDecision,
|
|
1109
|
+
confidence: 0.5 * agentState.rewardMultiplier,
|
|
848
1110
|
trigger: "bridge",
|
|
849
|
-
verdict: {
|
|
1111
|
+
verdict: {
|
|
1112
|
+
status: verdict.status, reason: verdict.reason, ruleId: verdict.ruleId,
|
|
1113
|
+
...(incentive ? { incentive } : {}),
|
|
1114
|
+
agentState: { cooldown: agentState.cooldownRemaining, influence: agentState.influence, penalties: agentState.totalPenalties, rewards: agentState.totalRewards },
|
|
1115
|
+
},
|
|
850
1116
|
}],
|
|
851
|
-
avgImpact:
|
|
852
|
-
maxVolatility:
|
|
1117
|
+
avgImpact: impactForDecision,
|
|
1118
|
+
maxVolatility: bridgeMetrics.volatility,
|
|
853
1119
|
dynamics: [],
|
|
854
|
-
interventionCount:
|
|
1120
|
+
interventionCount: effectiveDecision !== "ALLOW" && effectiveDecision !== "REWARD" ? 1 : 0,
|
|
1121
|
+
});
|
|
1122
|
+
// Also broadcast a stability update so the metric refreshes live
|
|
1123
|
+
broadcast({
|
|
1124
|
+
type: "bridge_metrics",
|
|
1125
|
+
stability: bridgeMetrics.stability,
|
|
1126
|
+
volatility: bridgeMetrics.volatility,
|
|
1127
|
+
evalCount: bridgeMetrics.evalCount,
|
|
1128
|
+
totalInterventions: bridgeMetrics.totalInterventions,
|
|
855
1129
|
});
|
|
856
1130
|
// Record in session
|
|
857
1131
|
currentSession.evaluations.push({
|
|
858
1132
|
actor: payload.actor, action: payload.action,
|
|
859
|
-
decision:
|
|
1133
|
+
decision: effectiveDecision,
|
|
860
1134
|
reason: verdict.reason ?? "", ruleId: verdict.ruleId ?? null,
|
|
861
1135
|
world: payload.world ?? currentSession.world,
|
|
862
1136
|
timestamp: Date.now(), payload: payload.payload,
|
|
1137
|
+
incentive,
|
|
863
1138
|
});
|
|
864
1139
|
currentSession.guardCount = customGuards.length + (nvWorld.guards?.guards?.length ?? 0);
|
|
865
1140
|
// Persist to audit trail on disk
|
|
@@ -867,23 +1142,32 @@ function startInteractiveServer(port, onReady) {
|
|
|
867
1142
|
agent: payload.actor,
|
|
868
1143
|
action: payload.action,
|
|
869
1144
|
actionType: payload.payload?.type ?? "unknown",
|
|
870
|
-
verdict:
|
|
1145
|
+
verdict: effectiveDecision,
|
|
871
1146
|
reason: verdict.reason ?? "",
|
|
872
1147
|
confidence: verdict.confidence ?? 0.5,
|
|
873
1148
|
rulesFired: verdict.ruleId ? [{
|
|
874
1149
|
id: verdict.ruleId,
|
|
875
1150
|
description: verdict.reason ?? "",
|
|
876
|
-
effect:
|
|
877
|
-
impactReduction:
|
|
1151
|
+
effect: effectiveDecision === "BLOCK" ? "blocked" : effectiveDecision === "PENALIZE" ? "blocked" : effectiveDecision === "MODIFY" ? "dampened" : effectiveDecision === "REWARD" ? "monitored" : "monitored",
|
|
1152
|
+
impactReduction: effectiveDecision === "BLOCK" ? 1 : effectiveDecision === "PENALIZE" ? 0.8 : effectiveDecision === "MODIFY" ? 0.5 : 0,
|
|
878
1153
|
}] : [],
|
|
879
1154
|
worldState: payload.world ?? currentSession.world,
|
|
880
1155
|
});
|
|
881
1156
|
jsonResponse(res, 200, {
|
|
882
|
-
decision,
|
|
1157
|
+
decision: effectiveDecision,
|
|
883
1158
|
reason: verdict.reason ?? null,
|
|
884
1159
|
rule_id: verdict.ruleId ?? null,
|
|
885
1160
|
evidence: verdict.evidence ?? null,
|
|
886
|
-
modified_action:
|
|
1161
|
+
modified_action: effectiveDecision === "MODIFY" ? payload.payload : null,
|
|
1162
|
+
consequence: verdict.consequence ?? null,
|
|
1163
|
+
reward: verdict.reward ?? null,
|
|
1164
|
+
agent_state: {
|
|
1165
|
+
cooldown_remaining: agentState.cooldownRemaining,
|
|
1166
|
+
influence: agentState.influence,
|
|
1167
|
+
reward_multiplier: agentState.rewardMultiplier,
|
|
1168
|
+
total_penalties: agentState.totalPenalties,
|
|
1169
|
+
total_rewards: agentState.totalRewards,
|
|
1170
|
+
},
|
|
887
1171
|
});
|
|
888
1172
|
}
|
|
889
1173
|
catch (err) {
|
|
@@ -923,6 +1207,59 @@ function startInteractiveServer(port, onReady) {
|
|
|
923
1207
|
}
|
|
924
1208
|
return;
|
|
925
1209
|
}
|
|
1210
|
+
// Generate a full governed world from plain-English rules
|
|
1211
|
+
// Returns a complete world: thesis, state variables, invariants, gates
|
|
1212
|
+
if (req.url === "/api/generate-world" && req.method === "POST") {
|
|
1213
|
+
try {
|
|
1214
|
+
const body = await readBody(req);
|
|
1215
|
+
const payload = JSON.parse(body);
|
|
1216
|
+
if (!payload.text) {
|
|
1217
|
+
jsonResponse(res, 400, { error: "text is required" });
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const { parseRulesFromText, policyToWorld } = await Promise.resolve().then(() => __importStar(require("./policyEngine")));
|
|
1221
|
+
const parsed = parseRulesFromText(payload.text);
|
|
1222
|
+
const worldDef = policyToWorld(parsed);
|
|
1223
|
+
const worldName = payload.name || "Custom World";
|
|
1224
|
+
const worldId = "custom-" + worldName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
1225
|
+
// Apply as the active world
|
|
1226
|
+
customGuards.length = 0;
|
|
1227
|
+
for (let i = 0; i < parsed.rules.length; i++) {
|
|
1228
|
+
const rule = parsed.rules[i];
|
|
1229
|
+
customGuards.push({
|
|
1230
|
+
id: worldDef.invariants[i]?.id ?? `custom-rule-${i}`,
|
|
1231
|
+
label: rule.description,
|
|
1232
|
+
description: rule.description,
|
|
1233
|
+
category: "custom",
|
|
1234
|
+
enforcement: (rule.category === "prohibit" || rule.category === "circuit_breaker") ? "block" : rule.category === "limit" ? "block" : "allow",
|
|
1235
|
+
immutable: false,
|
|
1236
|
+
intent_patterns: [...rule.keywords],
|
|
1237
|
+
default_enabled: true,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
jsonResponse(res, 200, {
|
|
1241
|
+
status: "generated",
|
|
1242
|
+
world: {
|
|
1243
|
+
id: worldId,
|
|
1244
|
+
title: worldName,
|
|
1245
|
+
thesis: worldDef.thesis,
|
|
1246
|
+
stateVariables: worldDef.state_variables,
|
|
1247
|
+
invariants: worldDef.invariants,
|
|
1248
|
+
gates: worldDef.gates ?? [],
|
|
1249
|
+
},
|
|
1250
|
+
parsed: {
|
|
1251
|
+
total: parsed.summary.total,
|
|
1252
|
+
enforced: parsed.summary.enforced,
|
|
1253
|
+
advisory: parsed.summary.advisory,
|
|
1254
|
+
},
|
|
1255
|
+
rulesApplied: customGuards.length,
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
catch (err) {
|
|
1259
|
+
jsonResponse(res, 400, { error: err?.message ?? "Invalid request" });
|
|
1260
|
+
}
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
926
1263
|
// Apply parsed rules to the active governance context
|
|
927
1264
|
if (req.url === "/api/apply-rules" && req.method === "POST") {
|
|
928
1265
|
try {
|
|
@@ -944,6 +1281,8 @@ function startInteractiveServer(port, onReady) {
|
|
|
944
1281
|
immutable: false,
|
|
945
1282
|
intent_patterns: rule.intent_patterns,
|
|
946
1283
|
default_enabled: true,
|
|
1284
|
+
...(rule.consequence ? { consequence: rule.consequence } : {}),
|
|
1285
|
+
...(rule.reward ? { reward: rule.reward } : {}),
|
|
947
1286
|
});
|
|
948
1287
|
}
|
|
949
1288
|
jsonResponse(res, 200, {
|
|
@@ -1050,7 +1389,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
1050
1389
|
// Exports the current world configuration (base world + custom rules + overrides) as a world file
|
|
1051
1390
|
if (req.url === "/api/export-world" && req.method === "GET") {
|
|
1052
1391
|
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
1053
|
-
const worldId = currentSession.world || "
|
|
1392
|
+
const worldId = currentSession.world || "social_simulation";
|
|
1054
1393
|
let baseWorld;
|
|
1055
1394
|
try {
|
|
1056
1395
|
baseWorld = resolveWorld(worldId);
|
|
@@ -1088,6 +1427,8 @@ function startInteractiveServer(port, onReady) {
|
|
|
1088
1427
|
const blocked = evals.filter(e => e.decision === "BLOCK").length;
|
|
1089
1428
|
const modified = evals.filter(e => e.decision === "MODIFY").length;
|
|
1090
1429
|
const allowed = evals.filter(e => e.decision === "ALLOW").length;
|
|
1430
|
+
const rewarded = evals.filter(e => e.decision === "REWARD").length;
|
|
1431
|
+
const penalized = evals.filter(e => e.decision === "PENALIZE").length;
|
|
1091
1432
|
const uniqueActors = [...new Set(evals.map(e => e.actor))];
|
|
1092
1433
|
const uniqueActions = [...new Set(evals.map(e => e.action))];
|
|
1093
1434
|
const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
|
|
@@ -1101,6 +1442,8 @@ function startInteractiveServer(port, onReady) {
|
|
|
1101
1442
|
blocked,
|
|
1102
1443
|
modified,
|
|
1103
1444
|
allowed,
|
|
1445
|
+
rewarded,
|
|
1446
|
+
penalized,
|
|
1104
1447
|
},
|
|
1105
1448
|
agents: uniqueActors,
|
|
1106
1449
|
actionTypes: uniqueActions,
|
|
@@ -1243,6 +1586,10 @@ function startInteractiveServer(port, onReady) {
|
|
|
1243
1586
|
guardCount: customGuards.length,
|
|
1244
1587
|
evaluations: [],
|
|
1245
1588
|
};
|
|
1589
|
+
// Reset bridge metrics counters
|
|
1590
|
+
bridgeEvalCount = 0;
|
|
1591
|
+
bridgeBlockCount = 0;
|
|
1592
|
+
bridgeModifyCount = 0;
|
|
1246
1593
|
jsonResponse(res, 200, {
|
|
1247
1594
|
status: "reset",
|
|
1248
1595
|
newSessionId: currentSession.id,
|
|
@@ -1303,6 +1650,308 @@ function startInteractiveServer(port, onReady) {
|
|
|
1303
1650
|
jsonResponse(res, 200, { adapters });
|
|
1304
1651
|
return;
|
|
1305
1652
|
}
|
|
1653
|
+
// ── Agent Behavior States ──
|
|
1654
|
+
// Returns per-agent incentive state (cooldowns, influence, penalties, rewards)
|
|
1655
|
+
if (req.url === "/api/agent-states" && req.method === "GET") {
|
|
1656
|
+
const states = {};
|
|
1657
|
+
for (const [id, state] of agentBehaviorStates) {
|
|
1658
|
+
states[id] = {
|
|
1659
|
+
...state,
|
|
1660
|
+
isFrozen: state.cooldownRemaining > 0,
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
jsonResponse(res, 200, {
|
|
1664
|
+
agents: states,
|
|
1665
|
+
totalTracked: agentBehaviorStates.size,
|
|
1666
|
+
frozen: [...agentBehaviorStates.values()].filter(s => s.cooldownRemaining > 0).length,
|
|
1667
|
+
});
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
// Tick agent cooldowns — called once per simulation round to decrement timers
|
|
1671
|
+
if (req.url === "/api/agent-states/tick" && req.method === "POST") {
|
|
1672
|
+
try {
|
|
1673
|
+
const nv = await Promise.resolve().then(() => __importStar(require("@neuroverseos/governance")));
|
|
1674
|
+
if (nv.tickAgentStates) {
|
|
1675
|
+
const updated = nv.tickAgentStates(agentBehaviorStates);
|
|
1676
|
+
// Update our map with ticked values
|
|
1677
|
+
for (const [id, state] of updated) {
|
|
1678
|
+
agentBehaviorStates.set(id, state);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
else {
|
|
1682
|
+
localTickAgentStates();
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
catch {
|
|
1686
|
+
localTickAgentStates();
|
|
1687
|
+
}
|
|
1688
|
+
const frozen = [...agentBehaviorStates.values()].filter(s => s.cooldownRemaining > 0).length;
|
|
1689
|
+
jsonResponse(res, 200, { status: "ticked", totalTracked: agentBehaviorStates.size, frozen });
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
// Bridge capabilities — returns what each bridge can/cannot control
|
|
1693
|
+
if (req.url === "/api/bridge-capabilities" && req.method === "GET") {
|
|
1694
|
+
const { SCIENCECLAW_CAPABILITIES } = await Promise.resolve().then(() => __importStar(require("../adapters/scienceclaw")));
|
|
1695
|
+
jsonResponse(res, 200, {
|
|
1696
|
+
bridges: [SCIENCECLAW_CAPABILITIES],
|
|
1697
|
+
});
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
// Bridge connection check — test if a bridge endpoint is reachable
|
|
1701
|
+
if (req.url === "/api/bridge-check" && req.method === "POST") {
|
|
1702
|
+
try {
|
|
1703
|
+
const body = await readBody(req);
|
|
1704
|
+
const { bridgeId, endpoint } = JSON.parse(body);
|
|
1705
|
+
if (bridgeId === "scienceclaw") {
|
|
1706
|
+
// Auto-detect scienceclaw-post on PATH
|
|
1707
|
+
let scienceClawPath = null;
|
|
1708
|
+
let scienceClawVersion = null;
|
|
1709
|
+
try {
|
|
1710
|
+
const whichCmd = process.platform === "win32" ? "where scienceclaw-post" : "which scienceclaw-post";
|
|
1711
|
+
scienceClawPath = (0, child_process_1.execSync)(whichCmd, { encoding: "utf-8", timeout: 3000 }).trim();
|
|
1712
|
+
}
|
|
1713
|
+
catch { /* not found */ }
|
|
1714
|
+
if (!scienceClawPath && endpoint) {
|
|
1715
|
+
// User provided a custom path/endpoint — check if it exists
|
|
1716
|
+
try {
|
|
1717
|
+
if (fs.existsSync(endpoint))
|
|
1718
|
+
scienceClawPath = endpoint;
|
|
1719
|
+
}
|
|
1720
|
+
catch { /* ignore */ }
|
|
1721
|
+
}
|
|
1722
|
+
if (scienceClawPath) {
|
|
1723
|
+
// Try to get version
|
|
1724
|
+
try {
|
|
1725
|
+
scienceClawVersion = (0, child_process_1.execSync)(`"${scienceClawPath}" --version 2>/dev/null || echo "unknown"`, { encoding: "utf-8", timeout: 3000 }).trim();
|
|
1726
|
+
}
|
|
1727
|
+
catch {
|
|
1728
|
+
scienceClawVersion = "detected";
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
jsonResponse(res, 200, {
|
|
1732
|
+
connected: true, // governance server is always ready
|
|
1733
|
+
bridgeId,
|
|
1734
|
+
detected: !!scienceClawPath,
|
|
1735
|
+
binaryPath: scienceClawPath,
|
|
1736
|
+
version: scienceClawVersion,
|
|
1737
|
+
endpoint: "this server",
|
|
1738
|
+
governanceReady: true,
|
|
1739
|
+
canLaunch: !!scienceClawPath,
|
|
1740
|
+
note: scienceClawPath
|
|
1741
|
+
? `ScienceClaw detected at ${scienceClawPath}. Ready to launch governed sessions.`
|
|
1742
|
+
: "ScienceClaw not found on PATH. Governance server ready — ScienceClaw can connect via /api/evaluate, or provide the binary path.",
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
jsonResponse(res, 400, { error: "Unknown bridge: " + bridgeId });
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
catch (err) {
|
|
1750
|
+
jsonResponse(res, 200, { connected: false, error: String(err) });
|
|
1751
|
+
}
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
// ── Bridge Launch — spawn governed ScienceClaw from the visualizer ──
|
|
1755
|
+
if (req.url === "/api/bridge-launch" && req.method === "POST") {
|
|
1756
|
+
try {
|
|
1757
|
+
const body = await readBody(req);
|
|
1758
|
+
const { bridgeId, args: launchArgs, binaryPath } = JSON.parse(body);
|
|
1759
|
+
if (bridgeId !== "scienceclaw") {
|
|
1760
|
+
jsonResponse(res, 400, { error: "Launch only supported for scienceclaw" });
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
// Find binary
|
|
1764
|
+
let bin = binaryPath || "scienceclaw-post";
|
|
1765
|
+
if (!binaryPath) {
|
|
1766
|
+
try {
|
|
1767
|
+
const whichCmd = process.platform === "win32" ? "where scienceclaw-post" : "which scienceclaw-post";
|
|
1768
|
+
bin = (0, child_process_1.execSync)(whichCmd, { encoding: "utf-8", timeout: 3000 }).trim();
|
|
1769
|
+
}
|
|
1770
|
+
catch {
|
|
1771
|
+
jsonResponse(res, 400, { error: "scienceclaw-post not found on PATH. Install ScienceClaw or provide binaryPath." });
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
// Kill any existing bridge process
|
|
1776
|
+
if (activeBridgeProcess) {
|
|
1777
|
+
try {
|
|
1778
|
+
activeBridgeProcess.kill();
|
|
1779
|
+
}
|
|
1780
|
+
catch { /* ignore */ }
|
|
1781
|
+
activeBridgeProcess = null;
|
|
1782
|
+
}
|
|
1783
|
+
// Build the governed wrapper command
|
|
1784
|
+
// We use the Python wrapper if available, otherwise call directly
|
|
1785
|
+
const wrapperPath = path.join(__dirname, "..", "..", "connectors", "nv_scienceclaw_wrapper.py");
|
|
1786
|
+
const addr = server.address();
|
|
1787
|
+
const nvPort = (addr && typeof addr === "object") ? addr.port : port;
|
|
1788
|
+
const cmdArgs = launchArgs ?? [];
|
|
1789
|
+
let child;
|
|
1790
|
+
let launchMethod;
|
|
1791
|
+
if (fs.existsSync(wrapperPath)) {
|
|
1792
|
+
// Launch through Python governance wrapper
|
|
1793
|
+
child = (0, child_process_1.spawn)("python3", [wrapperPath, ...cmdArgs], {
|
|
1794
|
+
env: {
|
|
1795
|
+
...process.env,
|
|
1796
|
+
NEUROVERSE_URL: `http://localhost:${nvPort}`,
|
|
1797
|
+
SCIENCECLAW_BIN: bin,
|
|
1798
|
+
},
|
|
1799
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1800
|
+
});
|
|
1801
|
+
launchMethod = "python-wrapper";
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
// Launch directly — governance via /api/evaluate endpoint
|
|
1805
|
+
child = (0, child_process_1.spawn)(bin, cmdArgs, {
|
|
1806
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1807
|
+
});
|
|
1808
|
+
launchMethod = "direct";
|
|
1809
|
+
}
|
|
1810
|
+
activeBridgeProcess = child;
|
|
1811
|
+
const pid = child.pid;
|
|
1812
|
+
// Stream output to SSE clients
|
|
1813
|
+
child.stdout?.on("data", (chunk) => {
|
|
1814
|
+
const text = chunk.toString();
|
|
1815
|
+
broadcast({ type: "bridge_output", source: "scienceclaw", stream: "stdout", text });
|
|
1816
|
+
// Try parsing JSON lines for live adapter integration
|
|
1817
|
+
for (const line of text.split("\n").filter(Boolean)) {
|
|
1818
|
+
if (line.startsWith("{")) {
|
|
1819
|
+
try {
|
|
1820
|
+
const data = JSON.parse(line);
|
|
1821
|
+
if (data.cycle || data.step) {
|
|
1822
|
+
broadcast({ type: "bridge_round", source: "scienceclaw", data });
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
catch { /* not JSON */ }
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
child.stderr?.on("data", (chunk) => {
|
|
1830
|
+
broadcast({ type: "bridge_output", source: "scienceclaw", stream: "stderr", text: chunk.toString() });
|
|
1831
|
+
});
|
|
1832
|
+
child.on("close", (code) => {
|
|
1833
|
+
activeBridgeProcess = null;
|
|
1834
|
+
broadcast({ type: "bridge_exit", source: "scienceclaw", code, pid });
|
|
1835
|
+
});
|
|
1836
|
+
child.on("error", (err) => {
|
|
1837
|
+
activeBridgeProcess = null;
|
|
1838
|
+
broadcast({ type: "bridge_error", source: "scienceclaw", error: err.message });
|
|
1839
|
+
});
|
|
1840
|
+
jsonResponse(res, 200, {
|
|
1841
|
+
launched: true,
|
|
1842
|
+
pid,
|
|
1843
|
+
binary: bin,
|
|
1844
|
+
launchMethod,
|
|
1845
|
+
args: cmdArgs,
|
|
1846
|
+
message: `ScienceClaw launched (PID ${pid}) with ${launchMethod} governance. Output streaming to dashboard.`,
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
catch (err) {
|
|
1850
|
+
jsonResponse(res, 500, { error: `Launch failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1851
|
+
}
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
// ── Bridge Stop — kill running bridge process ──
|
|
1855
|
+
if (req.url === "/api/bridge-stop" && req.method === "POST") {
|
|
1856
|
+
if (activeBridgeProcess) {
|
|
1857
|
+
const pid = activeBridgeProcess.pid;
|
|
1858
|
+
try {
|
|
1859
|
+
activeBridgeProcess.kill();
|
|
1860
|
+
}
|
|
1861
|
+
catch { /* ignore */ }
|
|
1862
|
+
activeBridgeProcess = null;
|
|
1863
|
+
jsonResponse(res, 200, { stopped: true, pid });
|
|
1864
|
+
}
|
|
1865
|
+
else {
|
|
1866
|
+
jsonResponse(res, 200, { stopped: false, message: "No active bridge process" });
|
|
1867
|
+
}
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
// ── Bridge Status — check if a bridge process is running ──
|
|
1871
|
+
if (req.url === "/api/bridge-status" && req.method === "GET") {
|
|
1872
|
+
jsonResponse(res, 200, {
|
|
1873
|
+
running: !!activeBridgeProcess,
|
|
1874
|
+
pid: activeBridgeProcess?.pid ?? null,
|
|
1875
|
+
});
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
// ── AI API Key Management ──
|
|
1879
|
+
// GET: returns current provider config (key masked)
|
|
1880
|
+
// POST: sets provider config for world file compiling and AI-powered features
|
|
1881
|
+
if (req.url === "/api/ai-config" && req.method === "GET") {
|
|
1882
|
+
jsonResponse(res, 200, {
|
|
1883
|
+
provider: userAIConfig.provider,
|
|
1884
|
+
hasKey: userAIConfig.apiKey.length > 0,
|
|
1885
|
+
keyPreview: userAIConfig.apiKey.length > 8
|
|
1886
|
+
? userAIConfig.apiKey.slice(0, 4) + "..." + userAIConfig.apiKey.slice(-4)
|
|
1887
|
+
: userAIConfig.apiKey.length > 0 ? "****" : "",
|
|
1888
|
+
baseUrl: userAIConfig.baseUrl ?? null,
|
|
1889
|
+
model: userAIConfig.model ?? null,
|
|
1890
|
+
});
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (req.url === "/api/ai-config" && req.method === "POST") {
|
|
1894
|
+
try {
|
|
1895
|
+
const body = await readBody(req);
|
|
1896
|
+
const payload = JSON.parse(body);
|
|
1897
|
+
if (payload.provider)
|
|
1898
|
+
userAIConfig.provider = payload.provider;
|
|
1899
|
+
if (payload.apiKey !== undefined)
|
|
1900
|
+
userAIConfig.apiKey = payload.apiKey;
|
|
1901
|
+
if (payload.baseUrl !== undefined)
|
|
1902
|
+
userAIConfig.baseUrl = payload.baseUrl || undefined;
|
|
1903
|
+
if (payload.model !== undefined)
|
|
1904
|
+
userAIConfig.model = payload.model || undefined;
|
|
1905
|
+
jsonResponse(res, 200, {
|
|
1906
|
+
status: "updated",
|
|
1907
|
+
provider: userAIConfig.provider,
|
|
1908
|
+
hasKey: userAIConfig.apiKey.length > 0,
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
catch {
|
|
1912
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1913
|
+
}
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
// Generate governance rules from thesis description
|
|
1917
|
+
if (req.url === "/api/generate-rules" && req.method === "POST") {
|
|
1918
|
+
try {
|
|
1919
|
+
const body = await readBody(req);
|
|
1920
|
+
const { thesis } = JSON.parse(body);
|
|
1921
|
+
if (!thesis || thesis.trim().length === 0) {
|
|
1922
|
+
jsonResponse(res, 400, { error: "thesis is required" });
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
// Use existing policy engine to generate a world from the thesis
|
|
1926
|
+
const { parseRulesFromText, policyToWorld } = await Promise.resolve().then(() => __importStar(require("./policyEngine")));
|
|
1927
|
+
const parsed = parseRulesFromText(thesis);
|
|
1928
|
+
const world = policyToWorld(parsed);
|
|
1929
|
+
// Also set custom guards so they're active for governance
|
|
1930
|
+
customGuards.length = 0;
|
|
1931
|
+
parsed.rules.forEach((r, i) => {
|
|
1932
|
+
customGuards.push({
|
|
1933
|
+
id: r.id || "user-rule-" + (i + 1),
|
|
1934
|
+
label: r.description,
|
|
1935
|
+
description: r.description,
|
|
1936
|
+
category: "user",
|
|
1937
|
+
enforcement: r.enforcement || "block",
|
|
1938
|
+
immutable: false,
|
|
1939
|
+
intent_patterns: r.intent_patterns || [],
|
|
1940
|
+
default_enabled: true,
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
jsonResponse(res, 200, {
|
|
1944
|
+
status: "generated",
|
|
1945
|
+
parsed: { total: parsed.rules.length, rules: parsed.rules },
|
|
1946
|
+
world,
|
|
1947
|
+
guardsApplied: customGuards.length,
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
catch (err) {
|
|
1951
|
+
jsonResponse(res, 400, { error: "Failed to generate rules: " + String(err) });
|
|
1952
|
+
}
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1306
1955
|
// Run simulation via live adapter (external process)
|
|
1307
1956
|
if (req.url === "/api/run-live" && req.method === "POST") {
|
|
1308
1957
|
if (isRunning) {
|
|
@@ -1321,7 +1970,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
1321
1970
|
jsonResponse(res, 200, { status: "started", adapter: payload.adapterId });
|
|
1322
1971
|
// Resolve world for governance evaluation
|
|
1323
1972
|
const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
|
|
1324
|
-
const resolved = resolveWorld(payload.worldId ?? "
|
|
1973
|
+
const resolved = resolveWorld(payload.worldId ?? "social_simulation");
|
|
1325
1974
|
const world = { ...resolved.world };
|
|
1326
1975
|
if (payload.stateOverrides) {
|
|
1327
1976
|
const updatedVars = world.state_variables.map(sv => {
|
|
@@ -1356,7 +2005,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
1356
2005
|
// Listen for rounds from the adapter
|
|
1357
2006
|
adapter.on("round", (liveRound) => {
|
|
1358
2007
|
// Build lookup of original verdicts from adapter's adaptation data.
|
|
1359
|
-
// When an external bridge
|
|
2008
|
+
// When an external bridge already applied governance,
|
|
1360
2009
|
// the verdict lives in adaptation.deltas — use it instead of re-evaluating
|
|
1361
2010
|
// (re-evaluating the MODIFIED action would return ALLOW, hiding the BLOCK).
|
|
1362
2011
|
const adaptationByAgent = new Map();
|
|
@@ -1636,42 +2285,80 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
1636
2285
|
.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; }
|
|
1637
2286
|
|
|
1638
2287
|
/* RIGHT PANEL — Simulation viewer */
|
|
1639
|
-
.viewer { display: grid; grid-template-rows: auto
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
.
|
|
1643
|
-
|
|
1644
|
-
.
|
|
1645
|
-
.
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
.
|
|
1649
|
-
.
|
|
1650
|
-
.
|
|
1651
|
-
.
|
|
1652
|
-
.
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
.
|
|
1658
|
-
.
|
|
1659
|
-
.
|
|
1660
|
-
|
|
1661
|
-
.
|
|
1662
|
-
.
|
|
1663
|
-
.
|
|
2288
|
+
.viewer { display: grid; grid-template-rows: auto auto auto auto auto; overflow-y: auto; height: 100%; gap: 0; }
|
|
2289
|
+
|
|
2290
|
+
/* Outcome panel */
|
|
2291
|
+
.outcome-panel { padding: 20px 24px 16px; border-bottom: 1px solid var(--border); }
|
|
2292
|
+
.outcome-statement { font-size: 16px; font-weight: 600; color: var(--text-primary); line-height: 1.4; margin-bottom: 12px; }
|
|
2293
|
+
.outcome-empty { font-size: 13px; color: var(--text-faint); font-style: italic; }
|
|
2294
|
+
.confidence-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 8px; }
|
|
2295
|
+
.confidence-card { background: var(--bg-surface); border: 1px solid var(--bg-elevated); border-radius: 6px; padding: 8px 10px; }
|
|
2296
|
+
.confidence-card .cc-label { font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
|
|
2297
|
+
.confidence-card .cc-value { font-size: 13px; font-weight: 600; }
|
|
2298
|
+
.confidence-card .cc-value.good { color: var(--green); }
|
|
2299
|
+
.confidence-card .cc-value.warn { color: var(--yellow); }
|
|
2300
|
+
.confidence-card .cc-value.bad { color: var(--red); }
|
|
2301
|
+
.outcome-context { font-size: 10px; color: var(--text-faint); margin-top: 6px; }
|
|
2302
|
+
|
|
2303
|
+
/* Behavior panel */
|
|
2304
|
+
.behavior-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
|
|
2305
|
+
.behavior-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
|
2306
|
+
.behavior-shifts { margin-bottom: 10px; }
|
|
2307
|
+
.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; }
|
|
2308
|
+
.behavior-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
|
|
2309
|
+
|
|
2310
|
+
.activity-toggle { font-size: 10px; color: var(--text-muted); cursor: pointer; padding: 6px 0; margin-top: 4px; }
|
|
2311
|
+
.activity-toggle:hover { color: var(--text-secondary); }
|
|
2312
|
+
.activity-timeline { display: none; max-height: 180px; overflow-y: auto; margin-top: 6px; }
|
|
2313
|
+
.activity-timeline.open { display: block; }
|
|
2314
|
+
.activity-item { font-size: 11px; color: var(--text-secondary); padding: 3px 0; border-bottom: 1px solid var(--border); }
|
|
2315
|
+
.activity-item:last-child { border-bottom: none; }
|
|
2316
|
+
.activity-agent { color: var(--blue); font-weight: 500; }
|
|
2317
|
+
|
|
2318
|
+
/* Why panel */
|
|
2319
|
+
.why-panel { padding: 16px 24px; border-bottom: 1px solid var(--border); }
|
|
2320
|
+
.why-panel h2 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
|
|
2321
|
+
.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; }
|
|
2322
|
+
.why-empty { font-size: 12px; color: var(--text-faint); font-style: italic; }
|
|
2323
|
+
|
|
2324
|
+
/* Export bar */
|
|
2325
|
+
.export-bar { display: flex; gap: 8px; padding: 12px 24px; border-bottom: 1px solid var(--border); background: var(--bg-secondary); }
|
|
2326
|
+
.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; }
|
|
2327
|
+
.export-btn:hover { background: var(--bg-elevated); color: var(--text-primary); }
|
|
2328
|
+
.audit-btn { margin-left: auto; color: var(--text-faint); }
|
|
2329
|
+
|
|
2330
|
+
/* Audit overlay */
|
|
2331
|
+
.audit-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; background: rgba(0,0,0,0.7); }
|
|
2332
|
+
.audit-overlay.open { display: flex; align-items: center; justify-content: center; }
|
|
2333
|
+
.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; }
|
|
2334
|
+
.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; }
|
|
2335
|
+
.audit-header h2 { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; }
|
|
2336
|
+
.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; }
|
|
2337
|
+
.audit-close:hover { color: var(--text-primary); }
|
|
2338
|
+
.audit-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
|
2339
|
+
.audit-section:last-child { border-bottom: none; }
|
|
2340
|
+
.audit-section h3 { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
|
2341
|
+
.audit-section .rule-card { margin-bottom: 6px; }
|
|
2342
|
+
|
|
2343
|
+
/* Print styles for PDF export */
|
|
2344
|
+
@media print {
|
|
2345
|
+
body { background: #fff !important; color: #111 !important; }
|
|
2346
|
+
.controls, .export-bar, .audit-overlay, .activity-toggle { display: none !important; }
|
|
2347
|
+
.viewer { overflow: visible !important; height: auto !important; }
|
|
2348
|
+
.outcome-panel, .behavior-panel, .why-panel { border: 1px solid #ddd !important; margin-bottom: 8px; border-radius: 6px; }
|
|
2349
|
+
.confidence-card { border: 1px solid #ddd !important; }
|
|
2350
|
+
* { color: #111 !important; background: #fff !important; border-color: #ddd !important; }
|
|
2351
|
+
.cc-value.good { color: #16a34a !important; }
|
|
2352
|
+
.cc-value.warn { color: #ca8a04 !important; }
|
|
2353
|
+
.cc-value.bad { color: #dc2626 !important; }
|
|
2354
|
+
}
|
|
1664
2355
|
.center-line { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
|
|
1665
2356
|
.verdict { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-left: 4px; }
|
|
1666
2357
|
.verdict.ALLOW { background: var(--green-bg); color: var(--green); }
|
|
1667
2358
|
.verdict.BLOCK { background: var(--red-bg); color: var(--red); }
|
|
1668
2359
|
.verdict.PAUSE { background: var(--yellow-bg); color: var(--yellow); }
|
|
1669
2360
|
|
|
1670
|
-
/*
|
|
1671
|
-
.chart-container { position: relative; height: 100%; min-height: 150px; }
|
|
1672
|
-
canvas { width: 100% !important; height: 100% !important; }
|
|
1673
|
-
|
|
1674
|
-
/* Simulation Trace */
|
|
2361
|
+
/* Simulation Trace (audit only) */
|
|
1675
2362
|
.trace-round { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
|
|
1676
2363
|
.trace-round-header { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--bg-surface); cursor: pointer; user-select: none; }
|
|
1677
2364
|
.trace-round-header:hover { background: var(--bg-elevated); }
|
|
@@ -1781,6 +2468,239 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
1781
2468
|
.integrate-endpoint { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
|
|
1782
2469
|
.integrate-endpoint code { color: var(--green); background: var(--bg-surface); padding: 1px 5px; border-radius: 3px; }
|
|
1783
2470
|
|
|
2471
|
+
/* Language tabs for integration code */
|
|
2472
|
+
.lang-tabs { display: flex; gap: 2px; margin-bottom: 6px; }
|
|
2473
|
+
.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; }
|
|
2474
|
+
.lang-tab:hover { color: var(--text-secondary); }
|
|
2475
|
+
.lang-tab.active { color: var(--accent); background: var(--bg-surface); border-color: var(--bg-elevated); }
|
|
2476
|
+
.lang-code-panel { display: none; }
|
|
2477
|
+
.lang-code-panel.active { display: block; }
|
|
2478
|
+
.integrate-hint { font-size: 10px; color: var(--text-muted); margin-top: 6px; line-height: 1.5; }
|
|
2479
|
+
.integrate-hint strong { color: var(--text-secondary); }
|
|
2480
|
+
.integrate-works { font-size: 10px; color: var(--text-faint); margin-top: 8px; line-height: 1.6; }
|
|
2481
|
+
.integrate-works strong { color: var(--text-muted); }
|
|
2482
|
+
|
|
2483
|
+
/* Framework selector tabs */
|
|
2484
|
+
.fw-tabs { display: flex; gap: 4px; margin-bottom: 10px; }
|
|
2485
|
+
.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; }
|
|
2486
|
+
.fw-tab:hover { border-color: var(--text-muted); color: var(--text-secondary); }
|
|
2487
|
+
.fw-tab.active { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
|
|
2488
|
+
.fw-tab .fw-sub { display: block; font-size: 8px; font-weight: 400; color: var(--text-faint); margin-top: 2px; }
|
|
2489
|
+
.fw-tab.active .fw-sub { color: var(--accent); opacity: 0.7; }
|
|
2490
|
+
.fw-panel { display: none; }
|
|
2491
|
+
.fw-panel.active { display: block; }
|
|
2492
|
+
|
|
2493
|
+
/* Architecture callout */
|
|
2494
|
+
.arch-callout { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; }
|
|
2495
|
+
.arch-callout .arch-title { font-size: 10px; font-weight: 700; color: var(--text-secondary); margin-bottom: 4px; }
|
|
2496
|
+
.arch-flow { font-size: 10px; color: var(--text-muted); font-family: monospace; line-height: 1.6; }
|
|
2497
|
+
.arch-flow .arch-yes { color: var(--green); }
|
|
2498
|
+
.arch-flow .arch-no { color: var(--red); }
|
|
2499
|
+
.arch-warn { font-size: 9px; color: var(--red); margin-top: 6px; padding: 4px 8px; background: #2d0606; border-radius: 4px; line-height: 1.5; }
|
|
2500
|
+
|
|
2501
|
+
/* Responsive / Adaptive Panels */
|
|
2502
|
+
@media (max-width: 1200px) {
|
|
2503
|
+
.layout { grid-template-columns: 300px 1fr; }
|
|
2504
|
+
.controls { padding: 12px; }
|
|
2505
|
+
}
|
|
2506
|
+
@media (max-width: 960px) {
|
|
2507
|
+
.layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; height: auto; min-height: 100vh; }
|
|
2508
|
+
.controls { border-right: none; border-bottom: 1px solid var(--border); max-height: 50vh; overflow-y: auto; }
|
|
2509
|
+
.viewer { min-height: 60vh; }
|
|
2510
|
+
.viewer-top { grid-template-columns: 1fr; }
|
|
2511
|
+
.viewer-mid { grid-template-columns: 1fr; }
|
|
2512
|
+
.metric-grid { grid-template-columns: repeat(2, 1fr); }
|
|
2513
|
+
}
|
|
2514
|
+
@media (max-width: 640px) {
|
|
2515
|
+
.header h1 { font-size: 14px; }
|
|
2516
|
+
.header .sub { font-size: 10px; }
|
|
2517
|
+
.controls { padding: 10px; }
|
|
2518
|
+
.ctrl-section h3 { font-size: 10px; }
|
|
2519
|
+
.confidence-grid { grid-template-columns: 1fr; }
|
|
2520
|
+
.agent-name { width: 80px; }
|
|
2521
|
+
.integrate-code { font-size: 9px; padding: 6px; }
|
|
2522
|
+
.scenario-btn { padding: 6px; }
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
/* Bridge Selector */
|
|
2526
|
+
.bridge-selector { display: flex; gap: 6px; margin-bottom: 8px; }
|
|
2527
|
+
.bridge-btn { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 12px 8px; background: var(--bg-surface); border: 2px solid var(--border); border-radius: 6px; cursor: pointer; transition: all 0.2s; font-family: inherit; color: var(--text-primary); }
|
|
2528
|
+
.bridge-btn:hover { border-color: var(--text-muted); }
|
|
2529
|
+
.bridge-btn.active { border-color: var(--accent); background: var(--accent-bg); }
|
|
2530
|
+
.bridge-name { font-size: 12px; font-weight: 700; }
|
|
2531
|
+
.bridge-sub { font-size: 9px; color: var(--text-muted); margin-top: 2px; }
|
|
2532
|
+
.bridge-btn.active .bridge-name { color: var(--accent); }
|
|
2533
|
+
.bridge-status { font-size: 11px; margin-top: 4px; }
|
|
2534
|
+
.bridge-setup { margin-top: 8px; animation: fadeIn 0.2s ease; }
|
|
2535
|
+
.bridge-instructions { font-size: 11px; color: var(--text-secondary); line-height: 1.6; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 4px; padding: 10px; margin-bottom: 8px; white-space: pre-wrap; }
|
|
2536
|
+
.bridge-connect-row { display: flex; gap: 6px; }
|
|
2537
|
+
.bridge-endpoint-input { flex: 1; 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; }
|
|
2538
|
+
.bridge-endpoint-input:focus { border-color: var(--accent); outline: none; }
|
|
2539
|
+
.btn-connect { background: var(--accent); color: #fff; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-family: inherit; font-size: 12px; font-weight: 600; }
|
|
2540
|
+
.btn-connect:hover { filter: brightness(1.1); }
|
|
2541
|
+
.bridge-info-row { font-size: 11px; color: var(--text-secondary); padding: 4px 0; display: flex; align-items: center; gap: 6px; }
|
|
2542
|
+
.bridge-info-icon { font-size: 8px; }
|
|
2543
|
+
|
|
2544
|
+
/* World File Toggle */
|
|
2545
|
+
.worldfile-toggle { font-size: 11px; color: var(--text-muted); cursor: pointer; padding: 4px 0; transition: color 0.2s; }
|
|
2546
|
+
.worldfile-toggle:hover { color: var(--text-secondary); }
|
|
2547
|
+
.wf-arrow { font-size: 9px; margin-right: 4px; display: inline-block; transition: transform 0.2s; }
|
|
2548
|
+
.worldfile-hint { font-size: 10px; color: var(--text-faint); margin-top: 8px; line-height: 1.5; }
|
|
2549
|
+
.worldfile-hint strong { color: var(--accent); }
|
|
2550
|
+
.worldfile-hint code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
|
|
2551
|
+
|
|
2552
|
+
/* Governance Suggestions */
|
|
2553
|
+
.governance-suggestions { }
|
|
2554
|
+
.suggestion-item { background: var(--bg-surface); border-radius: 4px; padding: 8px 10px; margin-bottom: 6px; animation: fadeIn 0.3s ease; }
|
|
2555
|
+
.suggestion-text { font-size: 11px; color: var(--text-secondary); line-height: 1.5; }
|
|
2556
|
+
|
|
2557
|
+
/* ============================================ */
|
|
2558
|
+
/* OBSERVATION DECK */
|
|
2559
|
+
/* ============================================ */
|
|
2560
|
+
|
|
2561
|
+
.deck-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px 8px; }
|
|
2562
|
+
.deck-title { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; letter-spacing: 0.5px; }
|
|
2563
|
+
.deck-status { font-size: 10px; color: var(--text-faint); padding: 3px 8px; border: 1px solid var(--border); border-radius: 10px; }
|
|
2564
|
+
.deck-status.live { color: var(--green); border-color: var(--green); }
|
|
2565
|
+
.deck-status.complete { color: var(--accent); border-color: var(--accent); }
|
|
2566
|
+
|
|
2567
|
+
.deck-section { padding: 0 20px 16px; }
|
|
2568
|
+
.deck-label { font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
|
|
2569
|
+
|
|
2570
|
+
/* Choice Landscape — bubble visualization */
|
|
2571
|
+
.choice-landscape { min-height: 180px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: center; position: relative; transition: all 0.3s ease; }
|
|
2572
|
+
.choice-empty { text-align: center; color: var(--text-faint); font-size: 11px; line-height: 1.6; }
|
|
2573
|
+
.choice-empty-icon { font-size: 32px; opacity: 0.3; margin-bottom: 8px; }
|
|
2574
|
+
|
|
2575
|
+
/* Choice Bubble */
|
|
2576
|
+
.choice-bubble { display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.6s cubic-bezier(0.25, 0.8, 0.25, 1); position: relative; cursor: default; }
|
|
2577
|
+
.choice-bubble .cb-count { font-weight: 800; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.4); }
|
|
2578
|
+
.choice-bubble .cb-label { font-size: 8px; color: rgba(255,255,255,0.85); text-align: center; max-width: 90%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; letter-spacing: 0.3px; }
|
|
2579
|
+
.choice-bubble .cb-pct { font-size: 8px; color: rgba(255,255,255,0.6); margin-top: 1px; }
|
|
2580
|
+
|
|
2581
|
+
/* Decision Flow — intended → rule → actual */
|
|
2582
|
+
.decision-flow { margin-top: 12px; }
|
|
2583
|
+
.flow-row { display: grid; grid-template-columns: 1fr 40px 1fr; gap: 0; align-items: center; margin-bottom: 6px; min-height: 32px; }
|
|
2584
|
+
.flow-intended { display: flex; align-items: center; gap: 6px; justify-content: flex-end; }
|
|
2585
|
+
.flow-actual { display: flex; align-items: center; gap: 6px; }
|
|
2586
|
+
.flow-bar { height: 24px; border-radius: 4px; display: flex; align-items: center; padding: 0 8px; font-size: 10px; font-weight: 600; min-width: 20px; transition: width 0.6s cubic-bezier(0.25, 0.8, 0.25, 1); }
|
|
2587
|
+
.flow-bar.intended { justify-content: flex-end; background: var(--bg-elevated); color: var(--text-secondary); border-radius: 4px 0 0 4px; }
|
|
2588
|
+
.flow-bar.actual-allow { background: #052e16; color: #4ade80; border-radius: 0 4px 4px 0; }
|
|
2589
|
+
.flow-bar.actual-block { background: #2d0606; color: #f87171; border-radius: 0 4px 4px 0; }
|
|
2590
|
+
.flow-bar.actual-modify { background: #2d2006; color: #fbbf24; border-radius: 0 4px 4px 0; }
|
|
2591
|
+
.flow-arrow { text-align: center; font-size: 12px; color: var(--text-faint); }
|
|
2592
|
+
.flow-arrow.blocked { color: var(--red); }
|
|
2593
|
+
.flow-arrow.modified { color: var(--yellow); }
|
|
2594
|
+
.flow-arrow.allowed { color: var(--green); }
|
|
2595
|
+
.flow-label { font-size: 9px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2596
|
+
.flow-count { font-size: 10px; font-weight: 700; min-width: 16px; }
|
|
2597
|
+
.flow-legend { display: flex; gap: 12px; margin-top: 8px; justify-content: center; }
|
|
2598
|
+
.flow-legend-item { font-size: 9px; color: var(--text-faint); display: flex; align-items: center; gap: 4px; }
|
|
2599
|
+
.flow-legend-dot { width: 6px; height: 6px; border-radius: 50%; }
|
|
2600
|
+
|
|
2601
|
+
/* Choice Round Label */
|
|
2602
|
+
.choice-round-label { font-size: 10px; color: var(--text-faint); text-align: center; margin-top: 6px; }
|
|
2603
|
+
|
|
2604
|
+
/* Choice Stream */
|
|
2605
|
+
.choice-stream { max-height: 160px; overflow-y: auto; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px; }
|
|
2606
|
+
.stream-empty { font-size: 11px; color: var(--text-faint); text-align: center; padding: 16px 0; }
|
|
2607
|
+
.stream-item { font-size: 10px; color: var(--text-secondary); padding: 3px 0; border-bottom: 1px solid var(--border); display: flex; gap: 8px; align-items: baseline; animation: streamIn 0.3s ease; }
|
|
2608
|
+
.stream-item:last-child { border-bottom: none; }
|
|
2609
|
+
@keyframes streamIn { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: translateX(0); } }
|
|
2610
|
+
.stream-agent { color: var(--accent); font-weight: 600; min-width: 80px; }
|
|
2611
|
+
.stream-action { flex: 1; }
|
|
2612
|
+
.stream-round { color: var(--text-faint); font-size: 9px; }
|
|
2613
|
+
.stream-count { font-size: 9px; color: var(--text-faint); }
|
|
2614
|
+
|
|
2615
|
+
/* Behavioral Drift — post-run comparison */
|
|
2616
|
+
.drift-verdict { font-size: 14px; font-weight: 700; color: var(--text-primary); line-height: 1.5; margin-bottom: 12px; }
|
|
2617
|
+
.drift-comparison { display: grid; gap: 8px; }
|
|
2618
|
+
.drift-row { display: grid; grid-template-columns: 120px 1fr 40px 1fr; gap: 8px; align-items: center; font-size: 11px; }
|
|
2619
|
+
.drift-action { color: var(--text-secondary); font-weight: 600; text-align: right; }
|
|
2620
|
+
.drift-bar-container { height: 20px; background: var(--bg-elevated); border-radius: 3px; overflow: hidden; position: relative; }
|
|
2621
|
+
.drift-bar { height: 100%; border-radius: 3px; transition: width 0.8s cubic-bezier(0.25, 0.8, 0.25, 1); display: flex; align-items: center; padding-left: 6px; font-size: 9px; font-weight: 700; color: rgba(255,255,255,0.9); }
|
|
2622
|
+
.drift-bar.early { background: linear-gradient(90deg, var(--text-muted), var(--text-faint)); }
|
|
2623
|
+
.drift-bar.late { background: linear-gradient(90deg, var(--accent), #6366f1); }
|
|
2624
|
+
.drift-arrow-col { text-align: center; font-size: 14px; color: var(--text-faint); }
|
|
2625
|
+
.drift-delta { font-size: 10px; font-weight: 700; text-align: center; }
|
|
2626
|
+
.drift-delta.up { color: var(--green); }
|
|
2627
|
+
.drift-delta.down { color: var(--red); }
|
|
2628
|
+
.drift-delta.flat { color: var(--text-faint); }
|
|
2629
|
+
|
|
2630
|
+
.drift-header-row { display: grid; grid-template-columns: 120px 1fr 40px 1fr; gap: 8px; font-size: 9px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
|
|
2631
|
+
.drift-header-row span:first-child { text-align: right; }
|
|
2632
|
+
.drift-header-row span:nth-child(3) { text-align: center; }
|
|
2633
|
+
|
|
2634
|
+
/* What-If section */
|
|
2635
|
+
.whatif-content { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
2636
|
+
.whatif-stat { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
|
|
2637
|
+
.whatif-num { font-size: 28px; font-weight: 800; }
|
|
2638
|
+
.whatif-num.bad { color: var(--red); }
|
|
2639
|
+
.whatif-num.good { color: var(--green); }
|
|
2640
|
+
.whatif-label { font-size: 12px; color: var(--text-secondary); }
|
|
2641
|
+
.whatif-compare { font-size: 11px; color: var(--text-muted); margin-top: 8px; line-height: 1.5; }
|
|
2642
|
+
|
|
2643
|
+
/* Start Here button */
|
|
2644
|
+
.btn-start-here { width: 100%; padding: 8px; margin-bottom: 12px; background: transparent; color: var(--accent); border: 1px dashed var(--accent); border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all 0.2s; letter-spacing: 0.5px; }
|
|
2645
|
+
.btn-start-here:hover { background: var(--accent-bg); border-style: solid; }
|
|
2646
|
+
|
|
2647
|
+
/* AI API Key Section — always visible */
|
|
2648
|
+
.ai-key-section { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 16px; }
|
|
2649
|
+
.ai-key-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
2650
|
+
.ai-key-label { font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; font-weight: 600; }
|
|
2651
|
+
.ai-key-status { font-size: 10px; padding: 2px 8px; border-radius: 10px; }
|
|
2652
|
+
.ai-key-status.connected { background: rgba(74, 222, 128, 0.15); color: #4ade80; }
|
|
2653
|
+
.ai-key-status.missing { background: rgba(248, 113, 113, 0.15); color: #f87171; }
|
|
2654
|
+
.ai-key-provider-row { display: flex; gap: 4px; margin-bottom: 8px; }
|
|
2655
|
+
.ai-key-provider-btn { flex: 1; padding: 5px 4px; background: var(--bg-secondary); color: var(--text-muted); border: 1px solid var(--border-subtle); border-radius: 4px; font-size: 10px; font-family: inherit; cursor: pointer; transition: all 0.2s; text-align: center; }
|
|
2656
|
+
.ai-key-provider-btn:hover { border-color: var(--text-muted); }
|
|
2657
|
+
.ai-key-provider-btn.active { background: var(--accent-bg); color: var(--accent); border-color: var(--accent); }
|
|
2658
|
+
.ai-key-input { width: 100%; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 11px; }
|
|
2659
|
+
.ai-key-input:focus { border-color: var(--accent); outline: none; }
|
|
2660
|
+
.ai-key-input::placeholder { color: var(--text-faint); }
|
|
2661
|
+
.ai-key-extra { margin-top: 6px; }
|
|
2662
|
+
.ai-key-extra input { width: 100%; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 5px 8px; border-radius: 4px; font-family: inherit; font-size: 10px; margin-bottom: 4px; }
|
|
2663
|
+
.ai-key-extra input:focus { border-color: var(--accent); outline: none; }
|
|
2664
|
+
.ai-key-save-row { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
|
|
2665
|
+
.btn-save-key { padding: 5px 12px; background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent); border-radius: 4px; font-size: 10px; font-family: inherit; cursor: pointer; transition: all 0.2s; }
|
|
2666
|
+
.btn-save-key:hover { filter: brightness(1.2); }
|
|
2667
|
+
.ai-key-hint { font-size: 9px; color: var(--text-faint); line-height: 1.4; }
|
|
2668
|
+
|
|
2669
|
+
/* Incentive indicators (reward/penalize) */
|
|
2670
|
+
.incentive-badge { display: inline-flex; align-items: center; gap: 3px; font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 10px; }
|
|
2671
|
+
.incentive-badge.reward { background: rgba(74, 222, 128, 0.15); color: #4ade80; }
|
|
2672
|
+
.incentive-badge.penalize { background: rgba(251, 146, 60, 0.15); color: #fb923c; }
|
|
2673
|
+
.flow-arrow.rewarded { color: #4ade80; font-size: 16px; }
|
|
2674
|
+
.flow-arrow.penalized { color: #fb923c; font-size: 16px; }
|
|
2675
|
+
.actual-reward { background: rgba(74, 222, 128, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); }
|
|
2676
|
+
.actual-penalize { background: rgba(251, 146, 60, 0.2); border: 1px solid rgba(251, 146, 60, 0.3); }
|
|
2677
|
+
|
|
2678
|
+
/* Onboarding Overlay */
|
|
2679
|
+
.onboard-overlay { position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.7); backdrop-filter: blur(6px); animation: fadeIn 0.3s ease; }
|
|
2680
|
+
.onboard-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 12px; max-width: 520px; width: 90vw; max-height: 85vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
|
2681
|
+
.onboard-header { padding: 24px 24px 0; }
|
|
2682
|
+
.onboard-header h2 { font-size: 18px; color: var(--text-primary); font-weight: 700; margin: 0; }
|
|
2683
|
+
.onboard-header .onboard-tagline { font-size: 13px; color: var(--text-secondary); margin-top: 6px; line-height: 1.5; }
|
|
2684
|
+
.onboard-body { padding: 16px 24px 24px; }
|
|
2685
|
+
.onboard-step { display: flex; gap: 12px; margin-bottom: 16px; }
|
|
2686
|
+
.onboard-step-num { min-width: 24px; height: 24px; background: var(--accent-bg); color: var(--accent); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; flex-shrink: 0; margin-top: 2px; }
|
|
2687
|
+
.onboard-step-content h4 { font-size: 13px; color: var(--text-primary); font-weight: 700; margin: 0 0 4px; }
|
|
2688
|
+
.onboard-step-content p { font-size: 11px; color: var(--text-muted); margin: 0; line-height: 1.5; }
|
|
2689
|
+
.onboard-example { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 12px; margin: 12px 0; }
|
|
2690
|
+
.onboard-example-label { font-size: 9px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
|
|
2691
|
+
.onboard-example-text { font-size: 12px; color: var(--accent); font-style: italic; line-height: 1.5; }
|
|
2692
|
+
.onboard-example-result { font-size: 10px; color: var(--text-muted); margin-top: 8px; line-height: 1.5; }
|
|
2693
|
+
.onboard-example-result strong { color: var(--text-secondary); }
|
|
2694
|
+
.onboard-section { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); }
|
|
2695
|
+
.onboard-section h4 { font-size: 11px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 1px; margin: 0 0 8px; }
|
|
2696
|
+
.onboard-capability { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
|
|
2697
|
+
.onboard-capability .cap-icon { color: var(--green); flex-shrink: 0; margin-top: 1px; }
|
|
2698
|
+
.onboard-footer { padding: 0 24px 20px; display: flex; gap: 8px; align-items: center; }
|
|
2699
|
+
.btn-onboard-start { flex: 1; padding: 12px; background: var(--accent); color: #fff; border: none; border-radius: 6px; font-size: 13px; font-weight: 700; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
|
|
2700
|
+
.btn-onboard-start:hover { filter: brightness(1.1); }
|
|
2701
|
+
.onboard-dismiss { font-size: 10px; color: var(--text-faint); cursor: pointer; white-space: nowrap; }
|
|
2702
|
+
.onboard-dismiss:hover { color: var(--text-muted); }
|
|
2703
|
+
|
|
1784
2704
|
/* Rule editor */
|
|
1785
2705
|
.rule-editor { margin-top: 8px; }
|
|
1786
2706
|
.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; }
|
|
@@ -1870,7 +2790,7 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
1870
2790
|
<body>
|
|
1871
2791
|
<div class="header">
|
|
1872
2792
|
<div style="display:flex;align-items:center">
|
|
1873
|
-
<h1>
|
|
2793
|
+
<h1>NeuroVerse</h1>
|
|
1874
2794
|
<span class="sub">Governance Runtime</span>
|
|
1875
2795
|
</div>
|
|
1876
2796
|
<div class="header-right">
|
|
@@ -1882,116 +2802,58 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
1882
2802
|
<div class="layout">
|
|
1883
2803
|
<!-- LEFT: CONTROLS -->
|
|
1884
2804
|
<div class="controls" id="controls-panel">
|
|
1885
|
-
<!-- World Action Bar -->
|
|
1886
|
-
<div class="world-action-bar">
|
|
1887
|
-
<button class="btn btn-world-action" id="new-world-btn" title="Clear everything and start fresh">+ New World</button>
|
|
1888
|
-
<button class="btn btn-world-action" id="load-file-btn" title="Load a .json world file">Load World File</button>
|
|
1889
|
-
<button class="btn btn-world-action" id="clear-rules-btn" title="Clear custom rules only">Clear Rules</button>
|
|
1890
|
-
<button class="btn btn-world-action btn-export" id="export-world-btn" title="Export current world as JSON">Save as World File</button>
|
|
1891
|
-
</div>
|
|
1892
2805
|
|
|
1893
|
-
<!--
|
|
1894
|
-
<
|
|
1895
|
-
<h3>World Source</h3>
|
|
1896
|
-
<div class="world-source-tabs">
|
|
1897
|
-
<label class="ws-tab active" data-source="preset">
|
|
1898
|
-
<input type="radio" name="world-source" value="preset" checked>
|
|
1899
|
-
<span class="ws-label">Preset</span>
|
|
1900
|
-
<span class="ws-hint">Demo scenarios</span>
|
|
1901
|
-
</label>
|
|
1902
|
-
<label class="ws-tab" data-source="custom">
|
|
1903
|
-
<input type="radio" name="world-source" value="custom">
|
|
1904
|
-
<span class="ws-label">Custom Rules</span>
|
|
1905
|
-
<span class="ws-hint">Define your world</span>
|
|
1906
|
-
</label>
|
|
1907
|
-
<label class="ws-tab" data-source="upload">
|
|
1908
|
-
<input type="radio" name="world-source" value="upload">
|
|
1909
|
-
<span class="ws-label">World File</span>
|
|
1910
|
-
<span class="ws-hint">JSON / .nv-world</span>
|
|
1911
|
-
</label>
|
|
1912
|
-
</div>
|
|
1913
|
-
</div>
|
|
2806
|
+
<!-- Start Here -->
|
|
2807
|
+
<button class="btn-start-here" id="start-here-btn">Start Here</button>
|
|
1914
2808
|
|
|
1915
|
-
<!--
|
|
1916
|
-
<div class="
|
|
1917
|
-
<div class="
|
|
1918
|
-
<
|
|
1919
|
-
<
|
|
1920
|
-
<select id="world-select"></select>
|
|
1921
|
-
</div>
|
|
1922
|
-
<div id="world-thesis" class="world-thesis"></div>
|
|
2809
|
+
<!-- AI API Key — always visible -->
|
|
2810
|
+
<div class="ai-key-section" id="ai-key-section">
|
|
2811
|
+
<div class="ai-key-header">
|
|
2812
|
+
<span class="ai-key-label">AI Runtime Key</span>
|
|
2813
|
+
<span class="ai-key-status missing" id="ai-key-status">No key</span>
|
|
1923
2814
|
</div>
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
<
|
|
1928
|
-
<div id="state-vars"></div>
|
|
2815
|
+
<div class="ai-key-provider-row">
|
|
2816
|
+
<button class="ai-key-provider-btn active" id="ai-prov-anthropic" data-provider="anthropic">Anthropic</button>
|
|
2817
|
+
<button class="ai-key-provider-btn" id="ai-prov-openai" data-provider="openai">OpenAI</button>
|
|
2818
|
+
<button class="ai-key-provider-btn" id="ai-prov-custom" data-provider="custom">Custom</button>
|
|
1929
2819
|
</div>
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
<
|
|
1934
|
-
<div id="scenario-list"></div>
|
|
2820
|
+
<input type="password" class="ai-key-input" id="ai-key-input" placeholder="sk-... or api key" autocomplete="off">
|
|
2821
|
+
<div class="ai-key-extra" id="ai-key-extra" style="display:none">
|
|
2822
|
+
<input type="text" id="ai-base-url" placeholder="Base URL (e.g. http://localhost:11434/v1)">
|
|
2823
|
+
<input type="text" id="ai-model" placeholder="Model name (e.g. gpt-4o, llama3)">
|
|
1935
2824
|
</div>
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
<div class="world-source-panel" id="source-custom" style="display:none">
|
|
1940
|
-
<div class="ctrl-section">
|
|
1941
|
-
<h3>Define Your World</h3>
|
|
1942
|
-
<div class="custom-world-header">
|
|
1943
|
-
<input type="text" class="world-name-input" id="custom-world-name" placeholder="World name (e.g. Marketing Governance)">
|
|
1944
|
-
<textarea class="world-thesis-input" id="custom-world-thesis" placeholder="What is this world about? (thesis)"></textarea>
|
|
1945
|
-
</div>
|
|
1946
|
-
<div class="rule-editor">
|
|
1947
|
-
<div class="rule-editor-label">Type your governance rules:</div>
|
|
1948
|
-
<textarea class="rule-input rule-input-large" id="rule-input" placeholder="No agent may spend more than $10k without approval All outbound emails must be reviewed Block deletion of production data Limit API calls to 100 per minute Require manager approval for refunds over $500"></textarea>
|
|
1949
|
-
<button class="btn btn-generate-world" id="parse-rules-btn">Generate World</button>
|
|
1950
|
-
<div id="parsed-rules" class="parsed-rules"></div>
|
|
1951
|
-
<div id="rule-status" class="rule-status"></div>
|
|
1952
|
-
<div class="rule-examples">
|
|
1953
|
-
Rule patterns:<br>
|
|
1954
|
-
<code>Block [action]</code> — hard suppression<br>
|
|
1955
|
-
<code>Limit [X] to [N]</code> — cap extremes<br>
|
|
1956
|
-
<code>Require [X] for [Y]</code> — structural constraint<br>
|
|
1957
|
-
<code>Pause [X] for review</code> — human-in-the-loop<br>
|
|
1958
|
-
<code>Allow [X]</code> — explicit permission<br>
|
|
1959
|
-
<code>Monitor [X]</code> — circuit breaker gate
|
|
1960
|
-
</div>
|
|
1961
|
-
</div>
|
|
2825
|
+
<div class="ai-key-save-row">
|
|
2826
|
+
<button class="btn-save-key" id="ai-key-save-btn">Save Key</button>
|
|
2827
|
+
<span class="ai-key-hint">Required for world file compiling & AI features</span>
|
|
1962
2828
|
</div>
|
|
2829
|
+
</div>
|
|
1963
2830
|
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
<div style="font-size:10px;color:var(--text-faint);margin-top:4px">Layer your rules on top of a preset world</div>
|
|
1972
|
-
</div>
|
|
1973
|
-
</div>
|
|
2831
|
+
<!-- STEP 1: What are you trying to control? -->
|
|
2832
|
+
<div class="ctrl-section">
|
|
2833
|
+
<h3>What are you trying to control?</h3>
|
|
2834
|
+
<textarea class="rule-input" id="thesis-input" placeholder="Describe what you want to govern in plain English. Examples: - Ensure no single agent dominates more than 15% of activity - Prevent panic-driven cascades in volatile markets - Block unverified research from being published - Require approval for transactions over $10k"></textarea>
|
|
2835
|
+
<button class="btn btn-generate-world" id="generate-rules-btn" style="margin-top:8px">Generate Governance Rules</button>
|
|
2836
|
+
<div id="rule-generation-status" class="rule-status"></div>
|
|
2837
|
+
<div id="generated-rules-preview" class="parsed-rules"></div>
|
|
1974
2838
|
</div>
|
|
1975
2839
|
|
|
1976
|
-
<!--
|
|
1977
|
-
<div class="
|
|
1978
|
-
<div class="
|
|
1979
|
-
<
|
|
2840
|
+
<!-- OR: Load World File -->
|
|
2841
|
+
<div class="ctrl-section">
|
|
2842
|
+
<div class="worldfile-toggle" id="worldfile-toggle">
|
|
2843
|
+
<span class="wf-arrow">▶</span> Or load a world file
|
|
2844
|
+
</div>
|
|
2845
|
+
<div id="worldfile-panel" style="display:none">
|
|
1980
2846
|
<div class="upload-zone" id="upload-zone">
|
|
1981
|
-
<div class="upload-icon">📄</div>
|
|
1982
2847
|
<div class="upload-label">Drop a .json or .nv-world file here</div>
|
|
1983
2848
|
<div class="upload-or">or</div>
|
|
1984
2849
|
<button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
|
|
1985
2850
|
<input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
|
|
1986
2851
|
</div>
|
|
1987
|
-
<div class="upload-paste-section">
|
|
1988
|
-
<
|
|
1989
|
-
<textarea class="rule-input rule-input-large" id="world-json-input" placeholder='{ "name": "Marketing Governance", "thesis": "All marketing actions are governed", "invariants": [...], "rules": [...], "gates": [...] }'></textarea>
|
|
2852
|
+
<div class="upload-paste-section" style="margin-top:8px">
|
|
2853
|
+
<textarea class="rule-input" id="world-json-input" placeholder="Or paste world JSON here..." style="min-height:60px"></textarea>
|
|
1990
2854
|
</div>
|
|
1991
|
-
<button class="btn btn-load-world" id="load-world-btn">Load into Runtime</button>
|
|
2855
|
+
<button class="btn btn-load-world" id="load-world-btn" style="margin-top:6px">Load into Runtime</button>
|
|
1992
2856
|
<div id="upload-status" class="rule-status"></div>
|
|
1993
|
-
|
|
1994
|
-
<!-- Loaded world info -->
|
|
1995
2857
|
<div id="loaded-world-info" style="display:none">
|
|
1996
2858
|
<div class="loaded-world-card">
|
|
1997
2859
|
<div class="lw-name" id="lw-name"></div>
|
|
@@ -1999,65 +2861,54 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
1999
2861
|
<div class="lw-stats" id="lw-stats"></div>
|
|
2000
2862
|
</div>
|
|
2001
2863
|
</div>
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
<!-- World file schema reference -->
|
|
2005
|
-
<div class="ctrl-section">
|
|
2006
|
-
<h3>World File Schema</h3>
|
|
2007
|
-
<div class="schema-ref">
|
|
2008
|
-
<div class="schema-item"><code>name</code> — world name</div>
|
|
2009
|
-
<div class="schema-item"><code>thesis</code> — what this world is about</div>
|
|
2010
|
-
<div class="schema-item"><code>rules[]</code> — governance rules (plain English or structured)</div>
|
|
2011
|
-
<div class="schema-item"><code>invariants[]</code> — rules that always hold <code>{id, description}</code></div>
|
|
2012
|
-
<div class="schema-item"><code>gates[]</code> — viability thresholds <code>{id, label, condition, severity}</code></div>
|
|
2013
|
-
<div class="schema-item"><code>state_variables[]</code> — sliders <code>{id, label, type, range, default_value}</code></div>
|
|
2864
|
+
<div class="worldfile-hint">
|
|
2865
|
+
Build detailed world files at <strong>neuroverseos.com</strong> or via <code>npm install @neuroverseos/governance</code>
|
|
2014
2866
|
</div>
|
|
2015
2867
|
</div>
|
|
2016
2868
|
</div>
|
|
2017
2869
|
|
|
2018
|
-
<!--
|
|
2019
|
-
<div class="ctrl-section"
|
|
2020
|
-
<h3>
|
|
2021
|
-
<div class="
|
|
2022
|
-
<
|
|
2023
|
-
<
|
|
2024
|
-
|
|
2870
|
+
<!-- STEP 2: Connect to Simulator -->
|
|
2871
|
+
<div class="ctrl-section">
|
|
2872
|
+
<h3>Connect to Simulator</h3>
|
|
2873
|
+
<div class="bridge-selector">
|
|
2874
|
+
<button class="bridge-btn" id="bridge-scienceclaw" data-bridge="scienceclaw">
|
|
2875
|
+
<span class="bridge-name">ScienceClaw</span>
|
|
2876
|
+
<span class="bridge-sub">Research governance</span>
|
|
2877
|
+
</button>
|
|
2878
|
+
</div>
|
|
2879
|
+
<div id="bridge-status" class="bridge-status"></div>
|
|
2880
|
+
<div id="bridge-setup" class="bridge-setup" style="display:none">
|
|
2881
|
+
<div id="bridge-setup-content"></div>
|
|
2882
|
+
<div class="bridge-connect-row">
|
|
2883
|
+
<input type="text" id="bridge-endpoint" class="bridge-endpoint-input" placeholder="http://localhost:5001">
|
|
2884
|
+
<button class="btn btn-connect" id="bridge-connect-btn">Connect</button>
|
|
2885
|
+
</div>
|
|
2025
2886
|
</div>
|
|
2026
|
-
<div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
|
|
2027
2887
|
</div>
|
|
2028
2888
|
|
|
2029
|
-
<!--
|
|
2030
|
-
<div class="ctrl-section">
|
|
2031
|
-
<h3>
|
|
2032
|
-
<div
|
|
2033
|
-
<select id="event-select"></select>
|
|
2034
|
-
<input type="number" id="event-round" min="1" max="20" value="3" placeholder="R">
|
|
2035
|
-
</div>
|
|
2036
|
-
<button class="btn btn-add" id="add-event-btn">+ Add Event</button>
|
|
2037
|
-
<div id="inject-list" class="inject-list" style="margin-top:8px"></div>
|
|
2889
|
+
<!-- STEP 3: Bridge-specific controls (shown after connection) -->
|
|
2890
|
+
<div class="ctrl-section" id="bridge-controls-section" style="display:none">
|
|
2891
|
+
<h3 id="bridge-controls-label">Settings</h3>
|
|
2892
|
+
<div id="bridge-controls"></div>
|
|
2038
2893
|
</div>
|
|
2039
2894
|
|
|
2040
|
-
<!--
|
|
2041
|
-
<div class="ctrl-section">
|
|
2042
|
-
<h3>
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
<span class="val" id="rounds-val">5</span>
|
|
2047
|
-
</div>
|
|
2048
|
-
<input type="range" id="rounds-slider" min="3" max="12" value="5">
|
|
2049
|
-
</div>
|
|
2895
|
+
<!-- Starting Conditions (shown when world has state variables) -->
|
|
2896
|
+
<div class="ctrl-section" id="state-vars-section" style="display:none">
|
|
2897
|
+
<h3 style="cursor:pointer" id="state-vars-toggle">
|
|
2898
|
+
<span class="wf-arrow" id="sv-arrow">▶</span> Starting Conditions
|
|
2899
|
+
</h3>
|
|
2900
|
+
<div id="state-vars" style="display:none"></div>
|
|
2050
2901
|
</div>
|
|
2051
2902
|
|
|
2052
2903
|
<!-- Run button -->
|
|
2053
|
-
<button class="btn btn-run" id="run-btn">Run
|
|
2904
|
+
<button class="btn btn-run" id="run-btn">Run Governance</button>
|
|
2054
2905
|
|
|
2055
|
-
<!-- Save as
|
|
2906
|
+
<!-- Save as Simulation Rules -->
|
|
2056
2907
|
<div id="save-section" style="margin-top:12px">
|
|
2057
|
-
<button class="btn btn-save" id="save-btn">Save as
|
|
2908
|
+
<button class="btn btn-save" id="save-btn">Save as Simulation Rules</button>
|
|
2058
2909
|
<div id="save-form" style="display:none;margin-top:8px">
|
|
2059
|
-
<input type="text" id="variant-name" placeholder="
|
|
2060
|
-
<input type="text" id="variant-desc" placeholder="What does this
|
|
2910
|
+
<input type="text" id="variant-name" placeholder="Name these rules (e.g. Strict Market Controls)" class="save-input">
|
|
2911
|
+
<input type="text" id="variant-desc" placeholder="What does this ruleset control?" class="save-input" style="margin-top:4px">
|
|
2061
2912
|
<div style="display:flex;gap:6px;margin-top:6px">
|
|
2062
2913
|
<button class="btn btn-confirm" id="confirm-save-btn">Save</button>
|
|
2063
2914
|
<button class="btn btn-cancel" id="cancel-save-btn">Cancel</button>
|
|
@@ -2065,48 +2916,28 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
2065
2916
|
</div>
|
|
2066
2917
|
</div>
|
|
2067
2918
|
|
|
2068
|
-
<!-- Saved
|
|
2919
|
+
<!-- Saved Simulation Rules -->
|
|
2069
2920
|
<div class="ctrl-section" style="margin-top:16px">
|
|
2070
|
-
<h3>Saved
|
|
2071
|
-
<div id="variant-list"><div style="font-size:11px;color
|
|
2921
|
+
<h3>Saved Rules</h3>
|
|
2922
|
+
<div id="variant-list"><div style="font-size:11px;color:var(--text-muted)">No saved rules yet</div></div>
|
|
2072
2923
|
</div>
|
|
2073
2924
|
|
|
2074
|
-
<!--
|
|
2075
|
-
<div class="ctrl-section" style="margin-top:16px">
|
|
2076
|
-
<h3>
|
|
2077
|
-
<div class="
|
|
2078
|
-
|
|
2079
|
-
<div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate
|
|
2080
|
-
|
|
2081
|
-
verdict = evaluate(
|
|
2082
|
-
actor=<span class="str">"agent_1"</span>,
|
|
2083
|
-
action=<span class="str">"panic_sell"</span>,
|
|
2084
|
-
world=<span class="str">"trading"</span>
|
|
2085
|
-
)
|
|
2086
|
-
|
|
2087
|
-
<span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
|
|
2088
|
-
action = <span class="str">"hold"</span> <span class="comment"># adapted</span></div>
|
|
2089
|
-
<div class="integrate-endpoint">
|
|
2090
|
-
Endpoint: <code id="integrate-url">POST /api/evaluate</code>
|
|
2091
|
-
</div>
|
|
2092
|
-
<div style="font-size:10px;color:#444;margin-top:6px">
|
|
2093
|
-
Fail-open · 500ms timeout · Stateless
|
|
2094
|
-
</div>
|
|
2095
|
-
<div style="margin-top:8px;font-size:10px">
|
|
2096
|
-
<span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> replaced
|
|
2097
|
-
<span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> constrained
|
|
2098
|
-
<span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
|
|
2099
|
-
</div>
|
|
2100
|
-
</div>
|
|
2925
|
+
<!-- Post-Run Governance Suggestions -->
|
|
2926
|
+
<div class="ctrl-section" id="governance-suggestions-section" style="display:none;margin-top:16px">
|
|
2927
|
+
<h3>Governance Suggestions</h3>
|
|
2928
|
+
<div id="governance-suggestions" class="governance-suggestions"></div>
|
|
2929
|
+
</div>
|
|
2101
2930
|
|
|
2102
2931
|
<!-- Session Report Panel -->
|
|
2103
2932
|
<div class="ctrl-section" id="session-panel">
|
|
2104
2933
|
<h3 class="ctrl-label">SESSION</h3>
|
|
2105
|
-
<div class="metric-grid" style="grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
|
|
2934
|
+
<div class="metric-grid" style="grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:8px">
|
|
2106
2935
|
<div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
|
|
2107
2936
|
<div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
|
|
2108
2937
|
<div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
|
|
2109
2938
|
<div class="metric-box"><div class="value" id="s-allowed" style="color:#4ade80">0</div><div class="label">Allowed</div></div>
|
|
2939
|
+
<div class="metric-box"><div class="value" id="s-rewarded" style="color:#4ade80">0</div><div class="label">Rewarded</div></div>
|
|
2940
|
+
<div class="metric-box"><div class="value" id="s-penalized" style="color:#fb923c">0</div><div class="label">Penalized</div></div>
|
|
2110
2941
|
</div>
|
|
2111
2942
|
<div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
|
|
2112
2943
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
@@ -2119,88 +2950,370 @@ verdict = evaluate(
|
|
|
2119
2950
|
</div>
|
|
2120
2951
|
</div>
|
|
2121
2952
|
|
|
2122
|
-
<!-- RIGHT:
|
|
2953
|
+
<!-- RIGHT: OBSERVATION DECK -->
|
|
2123
2954
|
<div class="viewer">
|
|
2124
|
-
<div class="
|
|
2125
|
-
<
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2955
|
+
<div class="deck-header">
|
|
2956
|
+
<h2 class="deck-title">Observation Deck</h2>
|
|
2957
|
+
<div class="deck-status" id="deck-status">Waiting for agents</div>
|
|
2958
|
+
</div>
|
|
2959
|
+
|
|
2960
|
+
<!-- LIVE: Agent Choices — bubble visualization -->
|
|
2961
|
+
<div class="deck-section" id="deck-choices-section">
|
|
2962
|
+
<div class="deck-label">Agent Choices</div>
|
|
2963
|
+
<div class="choice-landscape" id="choice-landscape">
|
|
2964
|
+
<div class="choice-empty" id="choice-empty">
|
|
2965
|
+
<div class="choice-empty-icon">◌</div>
|
|
2966
|
+
<div>Agents will appear here when the simulation runs.</div>
|
|
2967
|
+
<div style="margin-top:4px;color:var(--text-faint)">Each bubble = a type of decision. Size = how many agents chose it.</div>
|
|
2132
2968
|
</div>
|
|
2133
2969
|
</div>
|
|
2134
|
-
<div class="
|
|
2135
|
-
<h2>World Rules Active</h2>
|
|
2136
|
-
<div id="active-invariants"></div>
|
|
2137
|
-
</div>
|
|
2970
|
+
<div class="choice-round-label" id="choice-round-label" style="display:none">Round <span id="choice-round-num">1</span></div>
|
|
2138
2971
|
</div>
|
|
2139
2972
|
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
<
|
|
2144
|
-
|
|
2145
|
-
</div>
|
|
2973
|
+
<!-- LIVE: Choice Stream — latest agent decisions -->
|
|
2974
|
+
<div class="deck-section" id="deck-stream-section">
|
|
2975
|
+
<div class="deck-label">
|
|
2976
|
+
<span>Live Decisions</span>
|
|
2977
|
+
<span class="stream-count" id="stream-count"></span>
|
|
2146
2978
|
</div>
|
|
2147
|
-
<div class="
|
|
2148
|
-
<
|
|
2149
|
-
<div class="chart-container"><canvas id="chart"></canvas></div>
|
|
2979
|
+
<div class="choice-stream" id="choice-stream">
|
|
2980
|
+
<div class="stream-empty">Agent decisions will stream here in real time</div>
|
|
2150
2981
|
</div>
|
|
2151
2982
|
</div>
|
|
2152
2983
|
|
|
2153
|
-
<!--
|
|
2154
|
-
<div
|
|
2155
|
-
<div class="
|
|
2156
|
-
|
|
2157
|
-
|
|
2984
|
+
<!-- AFTER: Behavioral Drift — first vs last round -->
|
|
2985
|
+
<div class="deck-section" id="deck-drift-section" style="display:none">
|
|
2986
|
+
<div class="deck-label">Behavioral Drift</div>
|
|
2987
|
+
<div class="drift-verdict" id="drift-verdict"></div>
|
|
2988
|
+
<div class="drift-comparison" id="drift-comparison"></div>
|
|
2989
|
+
</div>
|
|
2990
|
+
|
|
2991
|
+
<!-- AFTER: Incentive Tracker (Reward/Penalize) — shown when governance engine supports it -->
|
|
2992
|
+
<div class="deck-section" id="deck-incentive-section" style="display:none">
|
|
2993
|
+
<div class="deck-label">Incentive Tracker</div>
|
|
2994
|
+
<div class="incentive-summary" id="incentive-summary"></div>
|
|
2995
|
+
<div class="incentive-agents" id="incentive-agents"></div>
|
|
2996
|
+
</div>
|
|
2997
|
+
|
|
2998
|
+
<!-- AFTER: What-If counterfactual -->
|
|
2999
|
+
<div class="deck-section" id="deck-whatif-section" style="display:none">
|
|
3000
|
+
<div class="deck-label">What If There Were No Rules?</div>
|
|
3001
|
+
<div class="whatif-content" id="whatif-content"></div>
|
|
3002
|
+
</div>
|
|
3003
|
+
|
|
3004
|
+
<!-- EXPORT -->
|
|
3005
|
+
<div class="export-bar">
|
|
3006
|
+
<button class="export-btn" onclick="exportPDF()">Download PDF</button>
|
|
3007
|
+
<button class="export-btn" onclick="exportCSV()">Export CSV</button>
|
|
3008
|
+
<button class="export-btn" onclick="copyShareSummary()">Copy Summary</button>
|
|
3009
|
+
<button class="export-btn audit-btn" onclick="openAudit()">Audit Trail</button>
|
|
3010
|
+
</div>
|
|
3011
|
+
|
|
3012
|
+
<!-- Hidden elements for data tracking (not displayed) -->
|
|
3013
|
+
<div style="display:none">
|
|
3014
|
+
<span id="m-stability">--</span>
|
|
3015
|
+
<span id="m-volatility">--</span>
|
|
3016
|
+
<span id="m-round">--</span>
|
|
3017
|
+
<span id="m-interventions">0</span>
|
|
3018
|
+
<span id="trace-source"></span>
|
|
3019
|
+
<div id="agents"></div>
|
|
3020
|
+
<div id="active-invariants"></div>
|
|
3021
|
+
<div id="log"></div>
|
|
3022
|
+
</div>
|
|
3023
|
+
</div>
|
|
3024
|
+
|
|
3025
|
+
<!-- LAYER 5: AUDIT OVERLAY (separate from dashboard) -->
|
|
3026
|
+
<div class="audit-overlay" id="audit-overlay" onclick="if(event.target===this)closeAudit()">
|
|
3027
|
+
<div class="audit-modal">
|
|
3028
|
+
<div class="audit-header">
|
|
3029
|
+
<h2>Audit Trail</h2>
|
|
3030
|
+
<button class="audit-close" onclick="closeAudit()">Close</button>
|
|
2158
3031
|
</div>
|
|
2159
|
-
<div class="
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
<span>Rule</span><span class="ss-flow-arrow">→</span>
|
|
2163
|
-
<span>Behavioral Shift</span><span class="ss-flow-arrow">→</span>
|
|
2164
|
-
<span>Emergent Pattern</span><span class="ss-flow-arrow">→</span>
|
|
2165
|
-
<span>System Outcome</span>
|
|
3032
|
+
<div class="audit-section" id="audit-rules">
|
|
3033
|
+
<h3>Active Rules</h3>
|
|
3034
|
+
<div id="audit-rules-content"></div>
|
|
2166
3035
|
</div>
|
|
2167
|
-
<div class="
|
|
2168
|
-
<
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
3036
|
+
<div class="audit-section" id="audit-verdicts">
|
|
3037
|
+
<h3>Verdict Log</h3>
|
|
3038
|
+
<div id="audit-verdicts-content"></div>
|
|
3039
|
+
</div>
|
|
3040
|
+
<div class="audit-section" id="audit-trace">
|
|
3041
|
+
<h3>Detailed Trace</h3>
|
|
3042
|
+
<div id="audit-trace-content"></div>
|
|
3043
|
+
</div>
|
|
3044
|
+
</div>
|
|
3045
|
+
</div>
|
|
3046
|
+
</div>
|
|
3047
|
+
|
|
3048
|
+
<!-- ONBOARDING OVERLAY (first visit) -->
|
|
3049
|
+
<div class="onboard-overlay" id="onboard-overlay" style="display:none">
|
|
3050
|
+
<div class="onboard-card">
|
|
3051
|
+
<div class="onboard-header">
|
|
3052
|
+
<h2>NeuroVerse Governance Runtime</h2>
|
|
3053
|
+
<div class="onboard-tagline">
|
|
3054
|
+
Define what your AI agents <em>cannot</em> do. Watch what happens when they try.
|
|
3055
|
+
</div>
|
|
3056
|
+
</div>
|
|
3057
|
+
<div class="onboard-body">
|
|
3058
|
+
|
|
3059
|
+
<div class="onboard-step">
|
|
3060
|
+
<div class="onboard-step-num">1</div>
|
|
3061
|
+
<div class="onboard-step-content">
|
|
3062
|
+
<h4>Say what you want to control</h4>
|
|
3063
|
+
<p>Type plain English. No config files, no YAML, no code.</p>
|
|
2173
3064
|
</div>
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
3065
|
+
</div>
|
|
3066
|
+
|
|
3067
|
+
<div class="onboard-example">
|
|
3068
|
+
<div class="onboard-example-label">Example</div>
|
|
3069
|
+
<div class="onboard-example-text">"No agent may sell more than 10% of its portfolio in a single round. Block any agent that attempts to front-run another's trade."</div>
|
|
3070
|
+
<div class="onboard-example-result">
|
|
3071
|
+
<strong>→</strong> Generates 2 governance rules: a <span style="color:#f87171">GATE</span> on portfolio concentration and a <span style="color:#f87171">BLOCK</span> on front-running patterns. These rules gate agent invocations at entry points before they execute.
|
|
2177
3072
|
</div>
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
3073
|
+
</div>
|
|
3074
|
+
|
|
3075
|
+
<div class="onboard-step">
|
|
3076
|
+
<div class="onboard-step-num">2</div>
|
|
3077
|
+
<div class="onboard-step-content">
|
|
3078
|
+
<h4>Connect your simulator</h4>
|
|
3079
|
+
<p>Plug in ScienceClaw (research governance) or any simulator. Governance works with any system that sends actions to the <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px;font-size:10px">/api/evaluate</code> endpoint.</p>
|
|
2181
3080
|
</div>
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
3081
|
+
</div>
|
|
3082
|
+
|
|
3083
|
+
<div class="onboard-step">
|
|
3084
|
+
<div class="onboard-step-num">3</div>
|
|
3085
|
+
<div class="onboard-step-content">
|
|
3086
|
+
<h4>Run and see what emerges</h4>
|
|
3087
|
+
<p>Agents act. Rules intervene. The right panel shows you <strong>what happened</strong>, <strong>what agents did</strong>, and <strong>why</strong> — not charts.</p>
|
|
2185
3088
|
</div>
|
|
2186
3089
|
</div>
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
<div class="
|
|
3090
|
+
|
|
3091
|
+
<div class="onboard-section">
|
|
3092
|
+
<h4>What you get</h4>
|
|
3093
|
+
<div class="onboard-capability"><span class="cap-icon">●</span> <span><strong>Behavioral reports</strong> — "7 agents attempted panic sells, all blocked. Stability improved 34 points." Plain language, not dashboards.</span></div>
|
|
3094
|
+
<div class="onboard-capability"><span class="cap-icon">●</span> <span><strong>Full audit trail</strong> — Every action, every verdict, every rule that fired. Exportable as CSV or PDF.</span></div>
|
|
3095
|
+
<div class="onboard-capability"><span class="cap-icon">●</span> <span><strong>Shape emergence</strong> — Change one rule, re-run, see how 1000 agents behave differently. Your rules define the boundaries where complex behavior can safely happen.</span></div>
|
|
3096
|
+
<div class="onboard-capability"><span class="cap-icon">●</span> <span><strong>Save & compare</strong> — Save rule configurations, reload them, compare results across runs.</span></div>
|
|
2192
3097
|
</div>
|
|
2193
3098
|
</div>
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
<
|
|
2197
|
-
<div id="log"></div>
|
|
3099
|
+
<div class="onboard-footer">
|
|
3100
|
+
<button class="btn-onboard-start" id="onboard-start-btn">Start</button>
|
|
3101
|
+
<span class="onboard-dismiss" id="onboard-dismiss">Don't show again</span>
|
|
2198
3102
|
</div>
|
|
2199
3103
|
</div>
|
|
2200
3104
|
</div>
|
|
2201
3105
|
|
|
2202
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"><\/script>
|
|
2203
3106
|
<script>
|
|
3107
|
+
// ============================================
|
|
3108
|
+
// OUTCOME INTELLIGENCE LAYER
|
|
3109
|
+
// ============================================
|
|
3110
|
+
|
|
3111
|
+
function generateOutcome(stability, volatility, shiftData, worldThesis) {
|
|
3112
|
+
var st = stability || 0;
|
|
3113
|
+
var vol = volatility || 0;
|
|
3114
|
+
var blockRatio = shiftData.total > 0 ? shiftData.blocks / shiftData.total : 0;
|
|
3115
|
+
var thesis = worldThesis || '';
|
|
3116
|
+
|
|
3117
|
+
// Determine outcome direction + dominant behavior (state + what agents did)
|
|
3118
|
+
var direction = '';
|
|
3119
|
+
var behavior = '';
|
|
3120
|
+
if (st > 0.7 && vol < 0.3) {
|
|
3121
|
+
direction = 'stabilized';
|
|
3122
|
+
behavior = 'agents shifted toward safer positions';
|
|
3123
|
+
} else if (st > 0.7 && vol >= 0.3) {
|
|
3124
|
+
direction = 'held under pressure';
|
|
3125
|
+
behavior = 'agents maintained cautious strategies despite volatility';
|
|
3126
|
+
} else if (st > 0.4 && vol < 0.5) {
|
|
3127
|
+
direction = 'partially converged';
|
|
3128
|
+
behavior = 'agents split between aggressive and conservative approaches';
|
|
3129
|
+
} else if (st <= 0.4 && vol >= 0.5) {
|
|
3130
|
+
direction = 'fragmented';
|
|
3131
|
+
behavior = 'agents competed with conflicting strategies';
|
|
3132
|
+
} else if (st <= 0.4) {
|
|
3133
|
+
direction = 'remains uncertain';
|
|
3134
|
+
behavior = 'agents failed to find a dominant strategy';
|
|
3135
|
+
} else {
|
|
3136
|
+
direction = 'showed mixed results';
|
|
3137
|
+
behavior = 'agents oscillated between risk-taking and caution';
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
// Enrich with world context if available
|
|
3141
|
+
if (thesis) {
|
|
3142
|
+
var nouns = thesis.match(/\b(market|research|supply|trade|financial|climate|regulatory|investment|innovation|security)\b/i);
|
|
3143
|
+
if (nouns) {
|
|
3144
|
+
var subject = nouns[1].charAt(0).toUpperCase() + nouns[1].slice(1).toLowerCase();
|
|
3145
|
+
return subject + ' ' + direction + ' as ' + behavior;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// Build behavior-enriched statement — always state + dominant behavior
|
|
3150
|
+
if (blockRatio > 0.4 && st > 0.6) return 'System ' + direction + ' after agents abandoned high-risk strategies';
|
|
3151
|
+
if (blockRatio > 0.2) return 'Outcome ' + direction + ' as ' + behavior;
|
|
3152
|
+
if (shiftData.patterns && shiftData.patterns.length > 0) return 'Outcome ' + direction + ' — ' + behavior;
|
|
3153
|
+
return 'Outcome ' + direction + ' as ' + behavior;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
function computeConfidence(stability, volatility, interventions, total) {
|
|
3157
|
+
var interventionRate = total > 0 ? interventions / total : 0;
|
|
3158
|
+
var score = stability * 0.5 + (1 - volatility) * 0.3 + (1 - interventionRate) * 0.2;
|
|
3159
|
+
|
|
3160
|
+
var strength, evidence, risk, strengthCls, evidenceCls, riskCls;
|
|
3161
|
+
|
|
3162
|
+
if (score > 0.75) {
|
|
3163
|
+
strength = 'Strong'; strengthCls = 'good';
|
|
3164
|
+
evidence = 'Solid'; evidenceCls = 'good';
|
|
3165
|
+
risk = 'Low'; riskCls = 'good';
|
|
3166
|
+
} else if (score > 0.55) {
|
|
3167
|
+
strength = 'Moderate'; strengthCls = 'warn';
|
|
3168
|
+
evidence = stability > 0.6 ? 'Solid' : 'Mixed'; evidenceCls = stability > 0.6 ? 'good' : 'warn';
|
|
3169
|
+
risk = volatility > 0.4 ? 'Elevated' : 'Moderate'; riskCls = volatility > 0.4 ? 'warn' : 'warn';
|
|
3170
|
+
} else if (score > 0.35) {
|
|
3171
|
+
strength = 'Moderate'; strengthCls = 'warn';
|
|
3172
|
+
evidence = 'Mixed'; evidenceCls = 'warn';
|
|
3173
|
+
risk = 'Elevated'; riskCls = 'bad';
|
|
3174
|
+
} else {
|
|
3175
|
+
strength = 'Weak'; strengthCls = 'bad';
|
|
3176
|
+
evidence = 'Thin'; evidenceCls = 'bad';
|
|
3177
|
+
risk = 'High'; riskCls = 'bad';
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
return { score: score, strength: strength, strengthCls: strengthCls, evidence: evidence, evidenceCls: evidenceCls, risk: risk, riskCls: riskCls };
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
function translateBehaviorNarrative(reaction, verdict) {
|
|
3184
|
+
// Convert verdict+action into before → after narrative — no system words
|
|
3185
|
+
var status = verdict ? verdict.status : 'ALLOW';
|
|
3186
|
+
var action = reaction || 'acted';
|
|
3187
|
+
if (status === 'BLOCK') {
|
|
3188
|
+
return 'abandoned ' + action + ' and switched to a safer strategy';
|
|
3189
|
+
}
|
|
3190
|
+
if (status === 'PENALIZE') {
|
|
3191
|
+
return 'was penalized for ' + action + ' and frozen for cooldown';
|
|
3192
|
+
}
|
|
3193
|
+
if (status === 'REWARD') {
|
|
3194
|
+
return 'was rewarded for ' + action + ' — behavior reinforced';
|
|
3195
|
+
}
|
|
3196
|
+
if (status === 'MODIFY' || status === 'PAUSE') {
|
|
3197
|
+
return 'scaled back ' + action + ' after early resistance';
|
|
3198
|
+
}
|
|
3199
|
+
return action;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
function generateBehaviorShifts(shiftData, bLog) {
|
|
3203
|
+
// Quantified behavioral shifts in human language
|
|
3204
|
+
var sentences = [];
|
|
3205
|
+
var total = shiftData.total || bLog.length || 0;
|
|
3206
|
+
if (total === 0) return sentences;
|
|
3207
|
+
|
|
3208
|
+
// Group by what agents actually did — always anchor in before → after
|
|
3209
|
+
var keys = Object.keys(shiftData.shifts || {}).sort(function(a, b) { return shiftData.shifts[b] - shiftData.shifts[a]; });
|
|
3210
|
+
if (keys.length > 0 && total > 0) {
|
|
3211
|
+
keys.slice(0, 3).forEach(function(k) {
|
|
3212
|
+
var parts = k.split(': ');
|
|
3213
|
+
var action = parts[1] || parts[0];
|
|
3214
|
+
var count = shiftData.shifts[k];
|
|
3215
|
+
var pct = Math.round((count / total) * 100);
|
|
3216
|
+
// before → after: what they tried → what they did instead
|
|
3217
|
+
if (parts[0] === 'BLOCK') {
|
|
3218
|
+
sentences.push(pct + '% of agents (' + count + ') shifted from ' + action + ' to conservative strategies');
|
|
3219
|
+
} else {
|
|
3220
|
+
sentences.push(pct + '% of agents (' + count + ') reduced ' + action + ' after initial attempts failed');
|
|
3221
|
+
}
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// Fallback from behavioral log — still before → after
|
|
3226
|
+
if (sentences.length === 0 && bLog.length > 0) {
|
|
3227
|
+
var blocked = bLog.filter(function(e) { return e.status === 'BLOCK'; }).length;
|
|
3228
|
+
var modified = bLog.filter(function(e) { return e.status === 'MODIFY' || e.status === 'PAUSE'; }).length;
|
|
3229
|
+
var penalized = bLog.filter(function(e) { return e.status === 'PENALIZE'; }).length;
|
|
3230
|
+
var rewarded = bLog.filter(function(e) { return e.status === 'REWARD'; }).length;
|
|
3231
|
+
var proceeded = bLog.length - blocked - modified - penalized;
|
|
3232
|
+
|
|
3233
|
+
if (blocked > 0) {
|
|
3234
|
+
var bPct = Math.round((blocked / bLog.length) * 100);
|
|
3235
|
+
sentences.push(bPct + '% of agents (' + blocked + ') shifted from aggressive to conservative strategies');
|
|
3236
|
+
}
|
|
3237
|
+
if (modified > 0) {
|
|
3238
|
+
var mPct = Math.round((modified / bLog.length) * 100);
|
|
3239
|
+
sentences.push(mPct + '% of agents (' + modified + ') reduced position size after initial attempts failed');
|
|
3240
|
+
}
|
|
3241
|
+
if (penalized > 0) {
|
|
3242
|
+
var penPct = Math.round((penalized / bLog.length) * 100);
|
|
3243
|
+
sentences.push(penPct + '% of agents (' + penalized + ') were penalized and frozen for cooldown periods');
|
|
3244
|
+
}
|
|
3245
|
+
if (rewarded > 0) {
|
|
3246
|
+
var rewPct = Math.round((rewarded / bLog.length) * 100);
|
|
3247
|
+
sentences.push(rewPct + '% of agents (' + rewarded + ') were rewarded for desirable behavior');
|
|
3248
|
+
}
|
|
3249
|
+
if (proceeded > 0 && (blocked > 0 || modified > 0 || penalized > 0)) {
|
|
3250
|
+
var pPct = Math.round((proceeded / bLog.length) * 100);
|
|
3251
|
+
sentences.push(pPct + '% of agents (' + proceeded + ') maintained their original strategy throughout');
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
// Volatility insight — before → after framing
|
|
3256
|
+
if (shiftData.governedVol < shiftData.baselineVol && shiftData.baselineVol > 0) {
|
|
3257
|
+
var volDrop = Math.round((1 - shiftData.governedVol / shiftData.baselineVol) * 100);
|
|
3258
|
+
sentences.push('Uncertainty dropped ' + volDrop + '% as agents moved from exploration to caution');
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// Add emergent pattern insights
|
|
3262
|
+
if (shiftData.patterns && shiftData.patterns.length > 0) {
|
|
3263
|
+
shiftData.patterns.slice(0, 2).forEach(function(p) {
|
|
3264
|
+
sentences.push(p);
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
return sentences;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
function generateCausation(shiftData, bLog) {
|
|
3272
|
+
// Pure causation — why behavior changed — no governance terminology
|
|
3273
|
+
var causes = [];
|
|
3274
|
+
var total = shiftData.total || bLog.length || 0;
|
|
3275
|
+
if (total === 0) return causes;
|
|
3276
|
+
|
|
3277
|
+
var blockRatio = shiftData.blocks / total;
|
|
3278
|
+
|
|
3279
|
+
// No system words (feedback, evaluation, validation, review)
|
|
3280
|
+
// Only agent experience (failed attempts, uncertainty, risk, delay)
|
|
3281
|
+
if (blockRatio > 0.4) {
|
|
3282
|
+
causes.push('Early aggressive attempts failed, forcing agents to rethink their strategy');
|
|
3283
|
+
} else if (blockRatio > 0.1) {
|
|
3284
|
+
causes.push('Some agents hit unexpected resistance and pulled back');
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
if (shiftData.governedVol < shiftData.baselineVol) {
|
|
3288
|
+
causes.push('Uncertainty dropped as agents stopped experimenting and committed to safer positions');
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
if (shiftData.patterns && shiftData.patterns.length > 0) {
|
|
3292
|
+
if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('hold') || p.toLowerCase().includes('caution'); })) {
|
|
3293
|
+
causes.push('Risk became too visible — agents chose to wait rather than act');
|
|
3294
|
+
}
|
|
3295
|
+
if (shiftData.patterns.some(function(p) { return p.toLowerCase().includes('coordination') || p.toLowerCase().includes('coordinated'); })) {
|
|
3296
|
+
causes.push('Agents independently converged on similar strategies under shared pressure');
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// Detect adaptation pattern from behavior log
|
|
3301
|
+
if (bLog.length >= 3) {
|
|
3302
|
+
var recent = bLog.slice(-6);
|
|
3303
|
+
var earlyBlocks = recent.slice(0, 3).filter(function(e) { return e.status === 'BLOCK'; }).length;
|
|
3304
|
+
var lateAllows = recent.slice(-3).filter(function(e) { return e.status === 'ALLOW'; }).length;
|
|
3305
|
+
if (earlyBlocks >= 2 && lateAllows >= 2) {
|
|
3306
|
+
causes.push('Agents became more cautious after early attempts failed');
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
if (causes.length === 0) {
|
|
3311
|
+
causes.push('Agents held steady — no major shifts in strategy throughout the simulation');
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
return causes;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
2204
3317
|
// ============================================
|
|
2205
3318
|
// STATE
|
|
2206
3319
|
// ============================================
|
|
@@ -2210,88 +3323,129 @@ let narratives = {};
|
|
|
2210
3323
|
let currentWorld = null;
|
|
2211
3324
|
let injectedEvents = [];
|
|
2212
3325
|
let totalInterventions = 0;
|
|
2213
|
-
let
|
|
2214
|
-
let governedImpacts = [];
|
|
2215
|
-
let chartLabels = [];
|
|
2216
|
-
let chart = null;
|
|
3326
|
+
let totalActions = 0;
|
|
2217
3327
|
let narrativeEventsByRound = {}; // { round: [{ id, headline, severity }] }
|
|
2218
3328
|
let ruleImpactTracker = {}; // { ruleId: { blocks: N, label: string } }
|
|
3329
|
+
let behaviorLog = []; // { agent, action, status, reason, ts }
|
|
3330
|
+
let latestStability = 0;
|
|
3331
|
+
let latestVolatility = 0;
|
|
3332
|
+
|
|
3333
|
+
// Bridge state
|
|
3334
|
+
let connectedBridge = null; // { id, label, capabilities, endpoint }
|
|
3335
|
+
let currentThesis = ''; // What the user is trying to control
|
|
3336
|
+
let currentRules = []; // Generated governance rules
|
|
3337
|
+
let bridgeCapabilities = {}; // { scienceclaw: {...} }
|
|
2219
3338
|
|
|
2220
3339
|
const statusEl = document.getElementById('status');
|
|
2221
|
-
const worldSelect = document.getElementById('world-select');
|
|
2222
3340
|
const stateVarsSection = document.getElementById('state-vars-section');
|
|
2223
3341
|
const stateVarsEl = document.getElementById('state-vars');
|
|
2224
|
-
const scenarioListEl = document.getElementById('scenario-list');
|
|
2225
|
-
const eventSelect = document.getElementById('event-select');
|
|
2226
|
-
const eventRoundInput = document.getElementById('event-round');
|
|
2227
|
-
const injectListEl = document.getElementById('inject-list');
|
|
2228
|
-
const roundsSlider = document.getElementById('rounds-slider');
|
|
2229
|
-
const roundsVal = document.getElementById('rounds-val');
|
|
2230
3342
|
const runBtn = document.getElementById('run-btn');
|
|
2231
3343
|
const agentsEl = document.getElementById('agents');
|
|
2232
3344
|
const logEl = document.getElementById('log');
|
|
2233
3345
|
const activeInvEl = document.getElementById('active-invariants');
|
|
2234
|
-
const engineSelect = document.getElementById('engine-select');
|
|
2235
|
-
const engineStatusEl = document.getElementById('engine-status');
|
|
2236
3346
|
const traceSourceEl = document.getElementById('trace-source');
|
|
3347
|
+
const outcomeStatementEl = document.getElementById('outcome-statement');
|
|
3348
|
+
const confidenceGridEl = document.getElementById('confidence-grid');
|
|
3349
|
+
const outcomeContextEl = document.getElementById('outcome-context');
|
|
3350
|
+
const behaviorShiftsEl = document.getElementById('behavior-shifts');
|
|
3351
|
+
const activityTimelineEl = document.getElementById('activity-timeline');
|
|
3352
|
+
const activityToggleEl = document.getElementById('activity-toggle');
|
|
3353
|
+
const whyContentEl = document.getElementById('why-content');
|
|
3354
|
+
|
|
3355
|
+
// Audit + Export
|
|
3356
|
+
function openAudit() {
|
|
3357
|
+
// Populate audit content from current data
|
|
3358
|
+
var rulesEl = document.getElementById('audit-rules-content');
|
|
3359
|
+
rulesEl.innerHTML = activeInvEl.innerHTML || '<div style="font-size:11px;color:#666">No rules loaded</div>';
|
|
3360
|
+
|
|
3361
|
+
var verdictsEl = document.getElementById('audit-verdicts-content');
|
|
3362
|
+
var vHtml = '';
|
|
3363
|
+
behaviorLog.forEach(function(e) {
|
|
3364
|
+
vHtml += '<div style="font-size:10px;padding:2px 0;display:flex;gap:8px;border-bottom:1px solid var(--border)">' +
|
|
3365
|
+
'<span style="color:var(--blue);min-width:100px">' + e.agent + '</span>' +
|
|
3366
|
+
'<span style="flex:1;color:var(--text-secondary)">' + e.action + '</span>' +
|
|
3367
|
+
'<span style="font-size:9px;font-weight:600;padding:0 4px;border-radius:2px;' +
|
|
3368
|
+
(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>' +
|
|
3369
|
+
(e.reason ? '<span style="font-size:9px;color:var(--text-faint)">' + e.reason + '</span>' : '') +
|
|
3370
|
+
'</div>';
|
|
3371
|
+
});
|
|
3372
|
+
verdictsEl.innerHTML = vHtml || '<div style="font-size:11px;color:#666">No verdict data</div>';
|
|
3373
|
+
|
|
3374
|
+
var traceEl = document.getElementById('audit-trace-content');
|
|
3375
|
+
traceEl.innerHTML = logEl.innerHTML || '<div style="font-size:11px;color:#666">No trace data</div>';
|
|
3376
|
+
|
|
3377
|
+
document.getElementById('audit-overlay').classList.add('open');
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
function closeAudit() {
|
|
3381
|
+
document.getElementById('audit-overlay').classList.remove('open');
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
function exportPDF() {
|
|
3385
|
+
window.print();
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function exportCSV() {
|
|
3389
|
+
if (behaviorLog.length === 0) { alert('No data to export yet'); return; }
|
|
3390
|
+
var csv = 'Agent,Action,Behavior,Round\\n';
|
|
3391
|
+
behaviorLog.forEach(function(e) {
|
|
3392
|
+
var behavior = translateBehaviorNarrative(e.action, { status: e.status });
|
|
3393
|
+
csv += '"' + e.agent + '","' + e.action + '","' + behavior + '","' + (e.round || '') + '"\\n';
|
|
3394
|
+
});
|
|
3395
|
+
var blob = new Blob([csv], { type: 'text/csv' });
|
|
3396
|
+
var url = URL.createObjectURL(blob);
|
|
3397
|
+
var a = document.createElement('a');
|
|
3398
|
+
a.href = url;
|
|
3399
|
+
a.download = 'simulation-behavior-' + new Date().toISOString().slice(0, 10) + '.csv';
|
|
3400
|
+
a.click();
|
|
3401
|
+
URL.revokeObjectURL(url);
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
function copyShareSummary() {
|
|
3405
|
+
var text = 'OBSERVATION DECK SUMMARY\\n\\n';
|
|
3406
|
+
|
|
3407
|
+
// Drift verdict
|
|
3408
|
+
var driftVerdict = document.getElementById('drift-verdict');
|
|
3409
|
+
if (driftVerdict && driftVerdict.textContent) {
|
|
3410
|
+
text += 'BEHAVIORAL DRIFT: ' + driftVerdict.textContent + '\\n\\n';
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
// What-if
|
|
3414
|
+
var whatifContent = document.getElementById('whatif-content');
|
|
3415
|
+
if (whatifContent && whatifContent.textContent) {
|
|
3416
|
+
text += 'COUNTERFACTUAL: ' + whatifContent.textContent + '\\n\\n';
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
// Basic stats
|
|
3420
|
+
text += 'Total agent actions: ' + totalActions + '\\n';
|
|
3421
|
+
text += 'Interventions: ' + totalInterventions + '\\n';
|
|
3422
|
+
text += '\\nGenerated by NeuroVerse Governance Runtime';
|
|
3423
|
+
|
|
3424
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
3425
|
+
alert('Summary copied to clipboard');
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
2237
3428
|
|
|
2238
3429
|
// ============================================
|
|
2239
|
-
// INIT — Load
|
|
3430
|
+
// INIT — Load bridge capabilities and saved rules
|
|
2240
3431
|
// ============================================
|
|
2241
3432
|
async function init() {
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
fetch('/api/
|
|
2245
|
-
fetch('/api/
|
|
2246
|
-
fetch('/api/adapters').then(r => r.json()).catch(() => ({ adapters: [] })),
|
|
3433
|
+
// Load bridge capabilities and worlds (for file uploads that reference presets)
|
|
3434
|
+
const [bcRes, wRes] = await Promise.all([
|
|
3435
|
+
fetch('/api/bridge-capabilities').then(r => r.json()).catch(() => ({ bridges: [] })),
|
|
3436
|
+
fetch('/api/worlds').then(r => r.json()).catch(() => ({ worlds: [] })),
|
|
2247
3437
|
]);
|
|
2248
3438
|
|
|
2249
3439
|
worlds = wRes.worlds;
|
|
2250
|
-
scenarios = sRes.scenarios;
|
|
2251
|
-
narratives = nRes.narratives;
|
|
2252
|
-
|
|
2253
|
-
// Populate engine selector with live adapters
|
|
2254
|
-
(aRes.adapters || []).forEach(function(a) {
|
|
2255
|
-
const opt = document.createElement('option');
|
|
2256
|
-
opt.value = a.id;
|
|
2257
|
-
opt.textContent = a.label;
|
|
2258
|
-
engineSelect.appendChild(opt);
|
|
2259
|
-
});
|
|
2260
3440
|
|
|
2261
|
-
//
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
opt.value = w.id;
|
|
2265
|
-
opt.textContent = w.title;
|
|
2266
|
-
worldSelect.appendChild(opt);
|
|
3441
|
+
// Index bridge capabilities
|
|
3442
|
+
(bcRes.bridges || []).forEach(function(b) {
|
|
3443
|
+
bridgeCapabilities[b.id] = b;
|
|
2267
3444
|
});
|
|
2268
3445
|
|
|
2269
|
-
//
|
|
2270
|
-
Object.entries(narratives).forEach(([id, ev]) => {
|
|
2271
|
-
const opt = document.createElement('option');
|
|
2272
|
-
opt.value = id;
|
|
2273
|
-
opt.textContent = ev.headline.slice(0, 40);
|
|
2274
|
-
eventSelect.appendChild(opt);
|
|
2275
|
-
});
|
|
2276
|
-
|
|
2277
|
-
// Populate scenario presets
|
|
2278
|
-
Object.entries(scenarios).forEach(([id, s]) => {
|
|
2279
|
-
const btn = document.createElement('button');
|
|
2280
|
-
btn.className = 'scenario-btn';
|
|
2281
|
-
btn.innerHTML = '<div class="stitle">' + s.title + '</div><div class="sdesc">' + s.description + '</div>';
|
|
2282
|
-
btn.onclick = () => loadScenario(id, s);
|
|
2283
|
-
scenarioListEl.appendChild(btn);
|
|
2284
|
-
});
|
|
2285
|
-
|
|
2286
|
-
// Select first world
|
|
2287
|
-
if (worlds.length > 0) selectWorld(worlds[0].id);
|
|
2288
|
-
|
|
2289
|
-
// Load saved variants
|
|
3446
|
+
// Load saved rules
|
|
2290
3447
|
await loadVariants();
|
|
2291
3448
|
|
|
2292
|
-
// Populate base world selector for custom rules mode
|
|
2293
|
-
populateBaseWorldSelect();
|
|
2294
|
-
|
|
2295
3449
|
// Connect SSE
|
|
2296
3450
|
connectSSE();
|
|
2297
3451
|
}
|
|
@@ -2300,114 +3454,384 @@ function selectWorld(worldId) {
|
|
|
2300
3454
|
currentWorld = worlds.find(w => w.id === worldId);
|
|
2301
3455
|
if (!currentWorld) return;
|
|
2302
3456
|
|
|
2303
|
-
|
|
2304
|
-
|
|
3457
|
+
// Render state variable controls if world has them
|
|
3458
|
+
renderStateVars();
|
|
3459
|
+
}
|
|
2305
3460
|
|
|
2306
|
-
|
|
2307
|
-
if (currentWorld.stateVariables
|
|
2308
|
-
stateVarsSection.style.display = '';
|
|
2309
|
-
stateVarsEl.innerHTML = '';
|
|
2310
|
-
currentWorld.stateVariables.forEach(sv => {
|
|
2311
|
-
const row = document.createElement('div');
|
|
2312
|
-
row.className = 'ctrl-row';
|
|
2313
|
-
|
|
2314
|
-
if (sv.type === 'number' && sv.range) {
|
|
2315
|
-
const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
|
|
2316
|
-
row.innerHTML =
|
|
2317
|
-
'<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
|
|
2318
|
-
'<input type="range" id="sv-' + sv.id + '" min="' + sv.range.min + '" max="' + sv.range.max + '" step="' + step + '" value="' + sv.default_value + '" data-sv="' + sv.id + '">';
|
|
2319
|
-
stateVarsEl.appendChild(row);
|
|
2320
|
-
const slider = row.querySelector('input');
|
|
2321
|
-
slider.addEventListener('input', () => {
|
|
2322
|
-
document.getElementById('sv-val-' + sv.id).textContent = slider.value;
|
|
2323
|
-
});
|
|
2324
|
-
} else if (sv.type === 'enum' && sv.enum_values) {
|
|
2325
|
-
row.innerHTML =
|
|
2326
|
-
'<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
|
|
2327
|
-
'<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
|
|
2328
|
-
sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
|
|
2329
|
-
'</select>';
|
|
2330
|
-
stateVarsEl.appendChild(row);
|
|
2331
|
-
} else if (sv.type === 'boolean') {
|
|
2332
|
-
row.innerHTML =
|
|
2333
|
-
'<div class="toggle-row">' +
|
|
2334
|
-
'<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
|
|
2335
|
-
'<span class="toggle-label">' + sv.label + '</span>' +
|
|
2336
|
-
'</div>';
|
|
2337
|
-
stateVarsEl.appendChild(row);
|
|
2338
|
-
const toggle = row.querySelector('.toggle');
|
|
2339
|
-
toggle.addEventListener('click', () => {
|
|
2340
|
-
toggle.classList.toggle('on');
|
|
2341
|
-
});
|
|
2342
|
-
}
|
|
2343
|
-
});
|
|
2344
|
-
} else {
|
|
3461
|
+
function renderStateVars() {
|
|
3462
|
+
if (!currentWorld || !currentWorld.stateVariables || currentWorld.stateVariables.length === 0) {
|
|
2345
3463
|
stateVarsSection.style.display = 'none';
|
|
3464
|
+
return;
|
|
2346
3465
|
}
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
3466
|
+
stateVarsSection.style.display = '';
|
|
3467
|
+
stateVarsEl.innerHTML = '';
|
|
3468
|
+
stateVarsEl.style.display = 'none'; // collapsed by default
|
|
3469
|
+
|
|
3470
|
+
currentWorld.stateVariables.forEach(sv => {
|
|
3471
|
+
const row = document.createElement('div');
|
|
3472
|
+
row.className = 'ctrl-row';
|
|
3473
|
+
|
|
3474
|
+
if (sv.type === 'number' && sv.range) {
|
|
3475
|
+
const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
|
|
3476
|
+
row.innerHTML =
|
|
3477
|
+
'<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
|
|
3478
|
+
'<input type="range" id="sv-' + sv.id + '" min="' + sv.range.min + '" max="' + sv.range.max + '" step="' + step + '" value="' + sv.default_value + '" data-sv="' + sv.id + '">';
|
|
3479
|
+
stateVarsEl.appendChild(row);
|
|
3480
|
+
const slider = row.querySelector('input');
|
|
3481
|
+
slider.addEventListener('input', () => {
|
|
3482
|
+
document.getElementById('sv-val-' + sv.id).textContent = slider.value;
|
|
3483
|
+
});
|
|
3484
|
+
} else if (sv.type === 'enum' && sv.enum_values) {
|
|
3485
|
+
row.innerHTML =
|
|
3486
|
+
'<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
|
|
3487
|
+
'<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
|
|
3488
|
+
sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
|
|
3489
|
+
'</select>';
|
|
3490
|
+
stateVarsEl.appendChild(row);
|
|
3491
|
+
} else if (sv.type === 'boolean') {
|
|
3492
|
+
row.innerHTML =
|
|
3493
|
+
'<div class="toggle-row">' +
|
|
3494
|
+
'<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
|
|
3495
|
+
'<span class="toggle-label">' + sv.label + '</span>' +
|
|
3496
|
+
'</div>';
|
|
3497
|
+
stateVarsEl.appendChild(row);
|
|
3498
|
+
const toggle = row.querySelector('.toggle');
|
|
3499
|
+
toggle.addEventListener('click', () => {
|
|
3500
|
+
toggle.classList.toggle('on');
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
});
|
|
2354
3504
|
}
|
|
2355
3505
|
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
}
|
|
3506
|
+
// State vars toggle (collapsed by default)
|
|
3507
|
+
document.getElementById('state-vars-toggle').addEventListener('click', () => {
|
|
3508
|
+
const el = stateVarsEl;
|
|
3509
|
+
const arrow = document.getElementById('sv-arrow');
|
|
3510
|
+
if (el.style.display === 'none') {
|
|
3511
|
+
el.style.display = '';
|
|
3512
|
+
arrow.innerHTML = '▼';
|
|
3513
|
+
} else {
|
|
3514
|
+
el.style.display = 'none';
|
|
3515
|
+
arrow.innerHTML = '▶';
|
|
3516
|
+
}
|
|
3517
|
+
});
|
|
2367
3518
|
|
|
2368
3519
|
// ============================================
|
|
2369
|
-
//
|
|
3520
|
+
// THESIS INPUT — Generate Governance Rules
|
|
2370
3521
|
// ============================================
|
|
2371
|
-
document.getElementById('
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
3522
|
+
const thesisInput = document.getElementById('thesis-input');
|
|
3523
|
+
const generateRulesBtn = document.getElementById('generate-rules-btn');
|
|
3524
|
+
const ruleGenStatusEl = document.getElementById('rule-generation-status');
|
|
3525
|
+
const generatedRulesPreviewEl = document.getElementById('generated-rules-preview');
|
|
3526
|
+
|
|
3527
|
+
generateRulesBtn.addEventListener('click', async () => {
|
|
3528
|
+
const thesis = thesisInput.value.trim();
|
|
3529
|
+
if (!thesis) return;
|
|
3530
|
+
|
|
3531
|
+
currentThesis = thesis;
|
|
3532
|
+
generateRulesBtn.disabled = true;
|
|
3533
|
+
generateRulesBtn.textContent = 'Generating...';
|
|
3534
|
+
ruleGenStatusEl.textContent = '';
|
|
2378
3535
|
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
el.addEventListener('click', () => {
|
|
2385
|
-
injectedEvents.splice(parseInt(el.dataset.idx), 1);
|
|
2386
|
-
renderInjectedEvents();
|
|
3536
|
+
try {
|
|
3537
|
+
const resp = await fetch('/api/generate-rules', {
|
|
3538
|
+
method: 'POST',
|
|
3539
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3540
|
+
body: JSON.stringify({ thesis }),
|
|
2387
3541
|
});
|
|
2388
|
-
|
|
2389
|
-
|
|
3542
|
+
const data = await resp.json();
|
|
3543
|
+
|
|
3544
|
+
if (data.error) {
|
|
3545
|
+
ruleGenStatusEl.textContent = data.error;
|
|
3546
|
+
ruleGenStatusEl.className = 'rule-status error';
|
|
3547
|
+
} else {
|
|
3548
|
+
currentRules = data.parsed.rules || [];
|
|
3549
|
+
ruleGenStatusEl.textContent = data.parsed.total + ' governance rule' + (data.parsed.total !== 1 ? 's' : '') + ' generated. Ready to run.';
|
|
3550
|
+
ruleGenStatusEl.className = 'rule-status success';
|
|
3551
|
+
|
|
3552
|
+
// Show preview of generated rules
|
|
3553
|
+
var iconMap = { block: '🔴', allow: '🟢', modify: '🔵', warn: '🟡', pause: '🟡' };
|
|
3554
|
+
generatedRulesPreviewEl.innerHTML = currentRules.map(function(r) {
|
|
3555
|
+
var icon = iconMap[r.enforcement] || '🟢';
|
|
3556
|
+
return '<div class="parsed-rule enforcement-' + r.enforcement + '">' +
|
|
3557
|
+
'<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + (r.enforcement || 'rule') + '</span></div>' +
|
|
3558
|
+
'<div class="pr-desc">' + r.description + '</div>' +
|
|
3559
|
+
'</div>';
|
|
3560
|
+
}).join('');
|
|
3561
|
+
|
|
3562
|
+
// Update active invariants display (hidden, for audit)
|
|
3563
|
+
activeInvEl.innerHTML = currentRules.map(function(r) {
|
|
3564
|
+
return '<div class="inv-item">[' + r.id + '] ' + r.description + '</div>';
|
|
3565
|
+
}).join('');
|
|
3566
|
+
|
|
3567
|
+
// If the generated world has state variables, show them
|
|
3568
|
+
if (data.world && data.world.stateVariables && data.world.stateVariables.length > 0) {
|
|
3569
|
+
currentWorld = {
|
|
3570
|
+
id: data.world.id || 'generated',
|
|
3571
|
+
title: data.world.title || 'Generated World',
|
|
3572
|
+
thesis: thesis,
|
|
3573
|
+
stateVariables: data.world.stateVariables,
|
|
3574
|
+
invariants: data.world.invariants || [],
|
|
3575
|
+
gates: data.world.gates || [],
|
|
3576
|
+
};
|
|
3577
|
+
renderStateVars();
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
} catch (err) {
|
|
3581
|
+
ruleGenStatusEl.textContent = 'Error: ' + err.message;
|
|
3582
|
+
ruleGenStatusEl.className = 'rule-status error';
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
generateRulesBtn.disabled = false;
|
|
3586
|
+
generateRulesBtn.textContent = 'Generate Governance Rules';
|
|
3587
|
+
});
|
|
2390
3588
|
|
|
2391
3589
|
// ============================================
|
|
2392
|
-
// WORLD
|
|
3590
|
+
// WORLD FILE TOGGLE
|
|
2393
3591
|
// ============================================
|
|
2394
|
-
|
|
2395
|
-
|
|
3592
|
+
document.getElementById('worldfile-toggle').addEventListener('click', () => {
|
|
3593
|
+
const panel = document.getElementById('worldfile-panel');
|
|
3594
|
+
const arrow = document.querySelector('#worldfile-toggle .wf-arrow');
|
|
3595
|
+
if (panel.style.display === 'none') {
|
|
3596
|
+
panel.style.display = '';
|
|
3597
|
+
arrow.innerHTML = '▼';
|
|
3598
|
+
} else {
|
|
3599
|
+
panel.style.display = 'none';
|
|
3600
|
+
arrow.innerHTML = '▶';
|
|
3601
|
+
}
|
|
3602
|
+
});
|
|
2396
3603
|
|
|
2397
3604
|
// ============================================
|
|
2398
|
-
//
|
|
3605
|
+
// BRIDGE CONNECTION
|
|
3606
|
+
// ============================================
|
|
3607
|
+
const bridgeButtons = document.querySelectorAll('.bridge-btn');
|
|
3608
|
+
const bridgeStatusEl = document.getElementById('bridge-status');
|
|
3609
|
+
const bridgeSetupEl = document.getElementById('bridge-setup');
|
|
3610
|
+
const bridgeSetupContentEl = document.getElementById('bridge-setup-content');
|
|
3611
|
+
const bridgeEndpointInput = document.getElementById('bridge-endpoint');
|
|
3612
|
+
const bridgeConnectBtn = document.getElementById('bridge-connect-btn');
|
|
3613
|
+
const bridgeControlsSection = document.getElementById('bridge-controls-section');
|
|
3614
|
+
const bridgeControlsEl = document.getElementById('bridge-controls');
|
|
3615
|
+
|
|
3616
|
+
// Track detected bridge binary info
|
|
3617
|
+
let detectedBridgeInfo = null; // { binaryPath, version, canLaunch }
|
|
3618
|
+
|
|
3619
|
+
bridgeButtons.forEach(btn => {
|
|
3620
|
+
btn.addEventListener('click', async () => {
|
|
3621
|
+
const bridgeId = btn.dataset.bridge;
|
|
3622
|
+
const caps = bridgeCapabilities[bridgeId];
|
|
3623
|
+
|
|
3624
|
+
// Highlight selected bridge
|
|
3625
|
+
bridgeButtons.forEach(b => b.classList.remove('active'));
|
|
3626
|
+
btn.classList.add('active');
|
|
3627
|
+
|
|
3628
|
+
if (bridgeId === 'scienceclaw') {
|
|
3629
|
+
// Auto-detect ScienceClaw — no manual setup needed
|
|
3630
|
+
bridgeSetupEl.style.display = '';
|
|
3631
|
+
bridgeEndpointInput.style.display = 'none';
|
|
3632
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--dim)">Detecting ScienceClaw...</span>';
|
|
3633
|
+
bridgeSetupContentEl.innerHTML = '';
|
|
3634
|
+
bridgeConnectBtn.disabled = true;
|
|
3635
|
+
|
|
3636
|
+
try {
|
|
3637
|
+
const resp = await fetch('/api/bridge-check', {
|
|
3638
|
+
method: 'POST',
|
|
3639
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3640
|
+
body: JSON.stringify({ bridgeId: 'scienceclaw', endpoint: '' }),
|
|
3641
|
+
});
|
|
3642
|
+
const data = await resp.json();
|
|
3643
|
+
detectedBridgeInfo = data;
|
|
3644
|
+
|
|
3645
|
+
if (data.detected && data.canLaunch) {
|
|
3646
|
+
// ScienceClaw found on PATH — show launch UI
|
|
3647
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--green)">ScienceClaw detected</span>';
|
|
3648
|
+
bridgeSetupContentEl.innerHTML =
|
|
3649
|
+
'<div class="bridge-instructions">' +
|
|
3650
|
+
'<div style="color:var(--green);margin-bottom:8px">Binary: ' + data.binaryPath + '</div>' +
|
|
3651
|
+
(data.version ? '<div style="color:var(--dim);margin-bottom:12px">Version: ' + data.version + '</div>' : '') +
|
|
3652
|
+
'<div style="margin-bottom:8px">Governance server ready. Click <strong>Launch</strong> to run a governed ScienceClaw session.</div>' +
|
|
3653
|
+
'<div class="bridge-launch-row" style="margin-top:12px">' +
|
|
3654
|
+
'<input type="text" id="bridge-sc-args" class="bridge-endpoint-input" placeholder="--agent MyAgent --topic "CRISPR risks"" style="display:block">' +
|
|
3655
|
+
'</div>' +
|
|
3656
|
+
'</div>';
|
|
3657
|
+
bridgeConnectBtn.textContent = 'Launch Governed Session';
|
|
3658
|
+
bridgeConnectBtn.disabled = false;
|
|
3659
|
+
} else {
|
|
3660
|
+
// ScienceClaw not auto-detected — show manual connection options
|
|
3661
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--accent)">ScienceClaw not detected on PATH</span>';
|
|
3662
|
+
bridgeSetupContentEl.innerHTML =
|
|
3663
|
+
'<div class="bridge-instructions">' +
|
|
3664
|
+
'<div style="margin-bottom:8px">ScienceClaw runs independently — NeuroVerse connects and governs it live.</div>' +
|
|
3665
|
+
'<div style="margin-bottom:4px"><strong>Option 1:</strong> Install ScienceClaw on your PATH, then re-click to auto-detect and launch directly from here.</div>' +
|
|
3666
|
+
'<div style="margin-bottom:4px"><strong>Option 2:</strong> Run the governance wrapper in a separate terminal:</div>' +
|
|
3667
|
+
'<div style="font-family:monospace;background:rgba(255,255,255,0.05);padding:8px;border-radius:4px;margin:8px 0">' +
|
|
3668
|
+
'python connectors/nv_scienceclaw_wrapper.py --agent MyAgent --topic "..."' +
|
|
3669
|
+
'</div>' +
|
|
3670
|
+
'<div style="color:var(--dim);margin-bottom:8px;font-size:0.85em">The wrapper launches ScienceClaw and streams each artifact cycle through governance in real time.</div>' +
|
|
3671
|
+
'<div style="margin-bottom:4px"><strong>Option 3:</strong> Point any running ScienceClaw instance at the governance endpoint:</div>' +
|
|
3672
|
+
'<div style="font-family:monospace;background:rgba(255,255,255,0.05);padding:8px;border-radius:4px;margin:8px 0">' +
|
|
3673
|
+
'POST /api/evaluate { actor, action, payload }' +
|
|
3674
|
+
'</div>' +
|
|
3675
|
+
'<div style="color:var(--dim);font-size:0.85em">Once ScienceClaw is running, click Connect to begin governed evaluation.</div>' +
|
|
3676
|
+
'</div>';
|
|
3677
|
+
bridgeConnectBtn.textContent = 'Connect';
|
|
3678
|
+
bridgeConnectBtn.disabled = false;
|
|
3679
|
+
}
|
|
3680
|
+
} catch (err) {
|
|
3681
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Detection failed: ' + err.message + '</span>';
|
|
3682
|
+
bridgeConnectBtn.disabled = false;
|
|
3683
|
+
}
|
|
3684
|
+
} else if (caps) {
|
|
3685
|
+
// Other bridges — show standard setup
|
|
3686
|
+
bridgeSetupEl.style.display = '';
|
|
3687
|
+
bridgeSetupContentEl.innerHTML = '<div class="bridge-instructions">' +
|
|
3688
|
+
caps.setupInstructions.replace(/\\n/g, '<br>') +
|
|
3689
|
+
'</div>';
|
|
3690
|
+
bridgeEndpointInput.value = 'http://localhost:5001';
|
|
3691
|
+
bridgeEndpointInput.style.display = '';
|
|
3692
|
+
bridgeConnectBtn.textContent = 'Connect';
|
|
3693
|
+
bridgeConnectBtn.disabled = false;
|
|
3694
|
+
bridgeStatusEl.innerHTML = '';
|
|
3695
|
+
}
|
|
3696
|
+
});
|
|
3697
|
+
});
|
|
3698
|
+
|
|
3699
|
+
bridgeConnectBtn.addEventListener('click', async () => {
|
|
3700
|
+
const activeBridge = document.querySelector('.bridge-btn.active');
|
|
3701
|
+
if (!activeBridge) return;
|
|
3702
|
+
const bridgeId = activeBridge.dataset.bridge;
|
|
3703
|
+
const endpoint = bridgeEndpointInput.value.trim();
|
|
3704
|
+
|
|
3705
|
+
bridgeConnectBtn.disabled = true;
|
|
3706
|
+
|
|
3707
|
+
if (bridgeId === 'scienceclaw' && detectedBridgeInfo && detectedBridgeInfo.canLaunch) {
|
|
3708
|
+
// ── LAUNCH governed ScienceClaw session ──
|
|
3709
|
+
bridgeConnectBtn.textContent = 'Launching...';
|
|
3710
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--dim)">Starting ScienceClaw with governance...</span>';
|
|
3711
|
+
|
|
3712
|
+
const scArgsInput = document.getElementById('bridge-sc-args');
|
|
3713
|
+
const scArgs = scArgsInput ? scArgsInput.value.trim().split(/\\s+/).filter(Boolean) : [];
|
|
3714
|
+
|
|
3715
|
+
try {
|
|
3716
|
+
const resp = await fetch('/api/bridge-launch', {
|
|
3717
|
+
method: 'POST',
|
|
3718
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3719
|
+
body: JSON.stringify({
|
|
3720
|
+
bridgeId: 'scienceclaw',
|
|
3721
|
+
args: scArgs.length > 0 ? scArgs : undefined,
|
|
3722
|
+
binaryPath: detectedBridgeInfo.binaryPath,
|
|
3723
|
+
}),
|
|
3724
|
+
});
|
|
3725
|
+
const data = await resp.json();
|
|
3726
|
+
|
|
3727
|
+
if (data.launched) {
|
|
3728
|
+
connectedBridge = {
|
|
3729
|
+
id: bridgeId,
|
|
3730
|
+
label: 'ScienceClaw',
|
|
3731
|
+
capabilities: bridgeCapabilities[bridgeId],
|
|
3732
|
+
endpoint: 'local (PID ' + data.pid + ')',
|
|
3733
|
+
};
|
|
3734
|
+
bridgeStatusEl.innerHTML =
|
|
3735
|
+
'<span style="color:var(--green)">ScienceClaw running (PID ' + data.pid + ')</span>' +
|
|
3736
|
+
'<br><span style="color:var(--dim);font-size:11px">Governance: ' + data.launchMethod + ' | Output streaming to dashboard</span>';
|
|
3737
|
+
bridgeSetupEl.style.display = 'none';
|
|
3738
|
+
|
|
3739
|
+
// Change connect button to stop button
|
|
3740
|
+
bridgeConnectBtn.textContent = 'Stop';
|
|
3741
|
+
bridgeConnectBtn.disabled = false;
|
|
3742
|
+
bridgeConnectBtn.onclick = async () => {
|
|
3743
|
+
await fetch('/api/bridge-stop', { method: 'POST' });
|
|
3744
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--dim)">ScienceClaw stopped</span>';
|
|
3745
|
+
bridgeConnectBtn.textContent = 'Launch Governed Session';
|
|
3746
|
+
bridgeConnectBtn.onclick = null; // reset to default handler
|
|
3747
|
+
};
|
|
3748
|
+
|
|
3749
|
+
showBridgeControls(bridgeId);
|
|
3750
|
+
} else {
|
|
3751
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Launch failed' + (data.error ? ': ' + data.error : '') + '</span>';
|
|
3752
|
+
}
|
|
3753
|
+
} catch (err) {
|
|
3754
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Launch error: ' + err.message + '</span>';
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
bridgeConnectBtn.disabled = false;
|
|
3758
|
+
if (bridgeConnectBtn.textContent === 'Launching...') bridgeConnectBtn.textContent = 'Launch Governed Session';
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// ── Standard connect flow (ScienceClaw without auto-detect, or other bridges) ──
|
|
3763
|
+
bridgeConnectBtn.textContent = 'Connecting...';
|
|
3764
|
+
bridgeStatusEl.innerHTML = '';
|
|
3765
|
+
|
|
3766
|
+
try {
|
|
3767
|
+
const resp = await fetch('/api/bridge-check', {
|
|
3768
|
+
method: 'POST',
|
|
3769
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3770
|
+
body: JSON.stringify({ bridgeId, endpoint }),
|
|
3771
|
+
});
|
|
3772
|
+
const data = await resp.json();
|
|
3773
|
+
|
|
3774
|
+
if (data.connected) {
|
|
3775
|
+
connectedBridge = {
|
|
3776
|
+
id: bridgeId,
|
|
3777
|
+
label: bridgeCapabilities[bridgeId]?.label || bridgeId,
|
|
3778
|
+
capabilities: bridgeCapabilities[bridgeId],
|
|
3779
|
+
endpoint: endpoint || 'local',
|
|
3780
|
+
};
|
|
3781
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--green)">Connected to ' + connectedBridge.label + '</span>';
|
|
3782
|
+
bridgeSetupEl.style.display = 'none';
|
|
3783
|
+
|
|
3784
|
+
showBridgeControls(bridgeId);
|
|
3785
|
+
} else {
|
|
3786
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Connection failed' + (data.error ? ': ' + data.error : '') + '</span>';
|
|
3787
|
+
}
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
bridgeStatusEl.innerHTML = '<span style="color:var(--red)">Error: ' + err.message + '</span>';
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
bridgeConnectBtn.disabled = false;
|
|
3793
|
+
bridgeConnectBtn.textContent = 'Connect';
|
|
3794
|
+
});
|
|
3795
|
+
|
|
3796
|
+
function showBridgeControls(bridgeId) {
|
|
3797
|
+
const caps = bridgeCapabilities[bridgeId];
|
|
3798
|
+
if (!caps) return;
|
|
3799
|
+
|
|
3800
|
+
bridgeControlsSection.style.display = '';
|
|
3801
|
+
document.getElementById('bridge-controls-label').textContent = caps.label + ' Settings';
|
|
3802
|
+
|
|
3803
|
+
var html = '';
|
|
3804
|
+
if (caps.governanceTiming === 'live') {
|
|
3805
|
+
html += '<div class="bridge-info-row"><span class="bridge-info-icon" style="color:var(--green)">●</span> Live governance — interventions happen during simulation</div>';
|
|
3806
|
+
} else {
|
|
3807
|
+
html += '<div class="bridge-info-row"><span class="bridge-info-icon" style="color:var(--accent)">●</span> Post-hoc governance — evaluation after each batch</div>';
|
|
3808
|
+
}
|
|
3809
|
+
if (caps.canHalt) {
|
|
3810
|
+
html += '<div class="bridge-info-row"><span class="bridge-info-icon">⏹</span> Can halt simulation if safety thresholds are exceeded</div>';
|
|
3811
|
+
}
|
|
3812
|
+
if (bridgeId === 'scienceclaw') {
|
|
3813
|
+
html += '<div class="bridge-info-row"><span class="bridge-info-icon" style="color:var(--green)">●</span> REWARD/PENALIZE incentive system active</div>';
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
bridgeControlsEl.innerHTML = html;
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
// ============================================
|
|
3820
|
+
// RUN GOVERNANCE
|
|
2399
3821
|
// ============================================
|
|
2400
3822
|
runBtn.addEventListener('click', async () => {
|
|
2401
|
-
if (!
|
|
3823
|
+
if (!connectedBridge && currentRules.length === 0) {
|
|
3824
|
+
ruleGenStatusEl.textContent = 'Generate governance rules or connect to a simulator first.';
|
|
3825
|
+
ruleGenStatusEl.className = 'rule-status error';
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
2402
3829
|
runBtn.disabled = true;
|
|
2403
3830
|
runBtn.textContent = 'Running...';
|
|
2404
3831
|
|
|
2405
3832
|
// Reset viewer state
|
|
2406
3833
|
totalInterventions = 0;
|
|
2407
|
-
|
|
2408
|
-
governedImpacts = [];
|
|
2409
|
-
chartLabels = [];
|
|
2410
|
-
if (chart) { chart.destroy(); chart = null; }
|
|
3834
|
+
totalActions = 0;
|
|
2411
3835
|
agentsEl.innerHTML = '';
|
|
2412
3836
|
logEl.innerHTML = '';
|
|
2413
3837
|
document.getElementById('m-stability').textContent = '--';
|
|
@@ -2415,9 +3839,9 @@ runBtn.addEventListener('click', async () => {
|
|
|
2415
3839
|
document.getElementById('m-round').textContent = '--';
|
|
2416
3840
|
document.getElementById('m-interventions').textContent = '0';
|
|
2417
3841
|
|
|
2418
|
-
// Gather state overrides
|
|
3842
|
+
// Gather state overrides from sliders (if world has them)
|
|
2419
3843
|
const stateOverrides = {};
|
|
2420
|
-
if (currentWorld.stateVariables) {
|
|
3844
|
+
if (currentWorld && currentWorld.stateVariables) {
|
|
2421
3845
|
currentWorld.stateVariables.forEach(sv => {
|
|
2422
3846
|
const el = document.getElementById('sv-' + sv.id);
|
|
2423
3847
|
if (!el) return;
|
|
@@ -2427,39 +3851,36 @@ runBtn.addEventListener('click', async () => {
|
|
|
2427
3851
|
});
|
|
2428
3852
|
}
|
|
2429
3853
|
|
|
2430
|
-
const selectedEngine = engineSelect.value;
|
|
2431
|
-
|
|
2432
3854
|
try {
|
|
2433
|
-
if (
|
|
2434
|
-
//
|
|
2435
|
-
const
|
|
2436
|
-
|
|
3855
|
+
if (connectedBridge) {
|
|
3856
|
+
// Run via bridge adapter
|
|
3857
|
+
const payload = {
|
|
3858
|
+
adapterId: connectedBridge.id,
|
|
3859
|
+
worldId: currentWorld ? currentWorld.id : 'social_simulation',
|
|
2437
3860
|
stateOverrides,
|
|
2438
|
-
injectEvents: injectedEvents.length > 0 ? injectedEvents : undefined,
|
|
2439
|
-
rounds: parseInt(roundsSlider.value),
|
|
2440
3861
|
};
|
|
2441
|
-
await fetch('/api/run-
|
|
3862
|
+
await fetch('/api/run-live', {
|
|
2442
3863
|
method: 'POST',
|
|
2443
3864
|
headers: { 'Content-Type': 'application/json' },
|
|
2444
|
-
body: JSON.stringify(
|
|
3865
|
+
body: JSON.stringify(payload),
|
|
2445
3866
|
});
|
|
2446
3867
|
} else {
|
|
2447
|
-
//
|
|
2448
|
-
const
|
|
2449
|
-
|
|
2450
|
-
worldId: currentWorld.id,
|
|
3868
|
+
// Fallback: run internal simulation with generated rules
|
|
3869
|
+
const config = {
|
|
3870
|
+
worldId: currentWorld ? currentWorld.id : 'social_simulation',
|
|
2451
3871
|
stateOverrides,
|
|
3872
|
+
rounds: 5,
|
|
2452
3873
|
};
|
|
2453
|
-
await fetch('/api/run-
|
|
3874
|
+
await fetch('/api/run-sim', {
|
|
2454
3875
|
method: 'POST',
|
|
2455
3876
|
headers: { 'Content-Type': 'application/json' },
|
|
2456
|
-
body: JSON.stringify(
|
|
3877
|
+
body: JSON.stringify(config),
|
|
2457
3878
|
});
|
|
2458
3879
|
}
|
|
2459
3880
|
} catch (err) {
|
|
2460
|
-
addLog('Error starting
|
|
3881
|
+
addLog('Error starting governance: ' + err.message, 'block');
|
|
2461
3882
|
runBtn.disabled = false;
|
|
2462
|
-
runBtn.textContent = 'Run
|
|
3883
|
+
runBtn.textContent = 'Run Governance';
|
|
2463
3884
|
}
|
|
2464
3885
|
});
|
|
2465
3886
|
|
|
@@ -2475,28 +3896,7 @@ function connectSSE() {
|
|
|
2475
3896
|
}
|
|
2476
3897
|
|
|
2477
3898
|
function initChart() {
|
|
2478
|
-
|
|
2479
|
-
const ctx = document.getElementById('chart');
|
|
2480
|
-
chart = new Chart(ctx, {
|
|
2481
|
-
type: 'line',
|
|
2482
|
-
data: {
|
|
2483
|
-
labels: chartLabels,
|
|
2484
|
-
datasets: [
|
|
2485
|
-
{ label: 'Baseline', data: baselineImpacts, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
|
|
2486
|
-
{ label: 'Governed', data: governedImpacts, borderColor: '#4ade80', backgroundColor: 'rgba(74,222,128,0.1)', fill: true, tension: 0.3, pointRadius: 3 },
|
|
2487
|
-
]
|
|
2488
|
-
},
|
|
2489
|
-
options: {
|
|
2490
|
-
animation: { duration: 400 },
|
|
2491
|
-
responsive: true,
|
|
2492
|
-
maintainAspectRatio: false,
|
|
2493
|
-
plugins: { legend: { labels: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888', font: { family: 'monospace', size: 10 } } } },
|
|
2494
|
-
scales: {
|
|
2495
|
-
x: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' } },
|
|
2496
|
-
y: { ticks: { color: getComputedStyle(document.body).getPropertyValue('--text-muted').trim() || '#888' }, grid: { color: getComputedStyle(document.body).getPropertyValue('--border').trim() || '#2a2a2a' }, min: -1, max: 1 }
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
});
|
|
3899
|
+
// Chart removed — no longer needed in outcome-first view
|
|
2500
3900
|
}
|
|
2501
3901
|
|
|
2502
3902
|
function addLog(msg, cls) {
|
|
@@ -2607,23 +4007,342 @@ function renderAgents(reactions) {
|
|
|
2607
4007
|
agentsEl.innerHTML = html;
|
|
2608
4008
|
}
|
|
2609
4009
|
|
|
4010
|
+
// ============================================
|
|
4011
|
+
// OBSERVATION DECK — Rendering
|
|
4012
|
+
// ============================================
|
|
4013
|
+
|
|
4014
|
+
// Per-round snapshots for behavioral drift
|
|
4015
|
+
var roundSnapshots = []; // [{ round, choices: { action: count }, flows: [{ from, to, count, status }] }]
|
|
4016
|
+
var firstRoundSnapshot = null;
|
|
4017
|
+
|
|
4018
|
+
function recordBehavior(reactions, round) {
|
|
4019
|
+
if (!reactions || !reactions.length) return;
|
|
4020
|
+
reactions.forEach(function(r) {
|
|
4021
|
+
var status = r.verdict ? r.verdict.status : 'ALLOW';
|
|
4022
|
+
var reason = r.verdict ? (r.verdict.reason || '') : '';
|
|
4023
|
+
behaviorLog.push({
|
|
4024
|
+
agent: r.stakeholder_id,
|
|
4025
|
+
action: r.reaction || 'acted',
|
|
4026
|
+
status: status,
|
|
4027
|
+
reason: reason,
|
|
4028
|
+
round: round || 0,
|
|
4029
|
+
ts: Date.now(),
|
|
4030
|
+
});
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
// Normalize action names into readable categories
|
|
4035
|
+
function normalizeAction(action) {
|
|
4036
|
+
if (!action) return 'act';
|
|
4037
|
+
var a = action.toLowerCase().replace(/_/g, ' ');
|
|
4038
|
+
// Collapse similar actions
|
|
4039
|
+
if (a.includes('panic sell') || a.includes('panic_sell')) return 'panic sell';
|
|
4040
|
+
if (a.includes('reduce') || a.includes('de-risk')) return 'reduce exposure';
|
|
4041
|
+
if (a.includes('hold') || a.includes('maintain') || a.includes('steady')) return 'hold steady';
|
|
4042
|
+
if (a.includes('aggressive') && a.includes('buy')) return 'aggressive buy';
|
|
4043
|
+
if (a.includes('buy') || a.includes('accumulate')) return 'buy';
|
|
4044
|
+
if (a.includes('sell') || a.includes('liquidat')) return 'sell';
|
|
4045
|
+
if (a.includes('diversif')) return 'diversify';
|
|
4046
|
+
if (a.includes('hedge')) return 'hedge';
|
|
4047
|
+
if (a.includes('wait') || a.includes('observe') || a.includes('monitor')) return 'wait & observe';
|
|
4048
|
+
if (a.includes('short')) return 'short';
|
|
4049
|
+
if (a.includes('post') || a.includes('create')) return 'create content';
|
|
4050
|
+
if (a.includes('follow') || a.includes('like') || a.includes('repost')) return 'engage';
|
|
4051
|
+
if (a.includes('publish') || a.includes('cite')) return 'publish';
|
|
4052
|
+
if (a.includes('analyze') || a.includes('research')) return 'research';
|
|
4053
|
+
// Return first 20 chars
|
|
4054
|
+
return a.slice(0, 20);
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
// Color for an action category
|
|
4058
|
+
function actionColor(action) {
|
|
4059
|
+
var colors = {
|
|
4060
|
+
'panic sell': '#ef4444', 'sell': '#f87171', 'short': '#dc2626',
|
|
4061
|
+
'aggressive buy': '#f59e0b', 'buy': '#22c55e', 'accumulate': '#22c55e',
|
|
4062
|
+
'hold steady': '#3b82f6', 'wait & observe': '#6366f1',
|
|
4063
|
+
'reduce exposure': '#a78bfa', 'diversify': '#8b5cf6', 'hedge': '#7c3aed',
|
|
4064
|
+
'create content': '#06b6d4', 'engage': '#14b8a6', 'publish': '#0ea5e9', 'research': '#818cf8',
|
|
4065
|
+
};
|
|
4066
|
+
return colors[action] || '#64748b';
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// ── Choice Bubbles ──
|
|
4070
|
+
function renderChoiceBubbles(reactions) {
|
|
4071
|
+
var landscapeEl = document.getElementById('choice-landscape');
|
|
4072
|
+
if (!landscapeEl) return;
|
|
4073
|
+
|
|
4074
|
+
// Group by normalized action
|
|
4075
|
+
var groups = {};
|
|
4076
|
+
reactions.forEach(function(r) {
|
|
4077
|
+
var action = normalizeAction(r.reaction);
|
|
4078
|
+
if (!groups[action]) groups[action] = { count: 0, totalImpact: 0 };
|
|
4079
|
+
groups[action].count++;
|
|
4080
|
+
groups[action].totalImpact += r.impact || 0;
|
|
4081
|
+
});
|
|
4082
|
+
|
|
4083
|
+
var total = reactions.length;
|
|
4084
|
+
var entries = Object.entries(groups).sort(function(a, b) { return b[1].count - a[1].count; });
|
|
4085
|
+
|
|
4086
|
+
// Hide empty state
|
|
4087
|
+
var emptyEl = document.getElementById('choice-empty');
|
|
4088
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
4089
|
+
|
|
4090
|
+
var html = entries.map(function(entry) {
|
|
4091
|
+
var action = entry[0];
|
|
4092
|
+
var data = entry[1];
|
|
4093
|
+
var pct = Math.round(data.count / total * 100);
|
|
4094
|
+
// Size: min 48px, max 120px, proportional to count
|
|
4095
|
+
var size = Math.max(48, Math.min(120, 48 + (data.count / total) * 100));
|
|
4096
|
+
var color = actionColor(action);
|
|
4097
|
+
var fontSize = size > 70 ? 18 : 14;
|
|
4098
|
+
return '<div class="choice-bubble" style="width:' + size + 'px;height:' + size + 'px;background:' + color + '" title="' + data.count + ' agents chose ' + action + '">' +
|
|
4099
|
+
'<div class="cb-count" style="font-size:' + fontSize + 'px">' + data.count + '</div>' +
|
|
4100
|
+
'<div class="cb-label">' + action + '</div>' +
|
|
4101
|
+
'<div class="cb-pct">' + pct + '%</div>' +
|
|
4102
|
+
'</div>';
|
|
4103
|
+
}).join('');
|
|
4104
|
+
|
|
4105
|
+
landscapeEl.innerHTML = html;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
// ── Decision Flow (rules in center, agents flowing around) ──
|
|
4109
|
+
function renderDecisionFlow(reactions) {
|
|
4110
|
+
var landscapeEl = document.getElementById('choice-landscape');
|
|
4111
|
+
if (!landscapeEl) return;
|
|
4112
|
+
|
|
4113
|
+
// Build flow data: what agents intended → what happened
|
|
4114
|
+
var flows = {}; // key: "intended|status|actual" → count
|
|
4115
|
+
var intendedCounts = {};
|
|
4116
|
+
var actualCounts = {};
|
|
4117
|
+
|
|
4118
|
+
reactions.forEach(function(r) {
|
|
4119
|
+
var intended = normalizeAction(r.reaction);
|
|
4120
|
+
var status = r.verdict ? r.verdict.status : 'ALLOW';
|
|
4121
|
+
var actual = intended; // for ALLOW, actual = intended
|
|
4122
|
+
if (status === 'BLOCK') actual = '(blocked)';
|
|
4123
|
+
else if (status === 'MODIFY') actual = normalizeAction(r.verdict && r.verdict.modified_action ? r.verdict.modified_action : r.reaction) + ' (adj)';
|
|
4124
|
+
else if (status === 'PENALIZE') actual = intended + ' (penalized)';
|
|
4125
|
+
else if (status === 'REWARD') actual = intended + ' (rewarded)';
|
|
4126
|
+
|
|
4127
|
+
var key = intended + '|' + status + '|' + actual;
|
|
4128
|
+
flows[key] = (flows[key] || 0) + 1;
|
|
4129
|
+
intendedCounts[intended] = (intendedCounts[intended] || 0) + 1;
|
|
4130
|
+
actualCounts[actual] = (actualCounts[actual] || 0) + 1;
|
|
4131
|
+
});
|
|
4132
|
+
|
|
4133
|
+
// Sort by count
|
|
4134
|
+
var sortedFlows = Object.entries(flows).sort(function(a, b) { return b[1] - a[1]; }).slice(0, 12);
|
|
4135
|
+
var total = reactions.length;
|
|
4136
|
+
var hasGovernance = sortedFlows.some(function(f) { return f[0].indexOf('BLOCK') >= 0 || f[0].indexOf('MODIFY') >= 0 || f[0].indexOf('REWARD') >= 0 || f[0].indexOf('PENALIZE') >= 0; });
|
|
4137
|
+
|
|
4138
|
+
if (!hasGovernance) {
|
|
4139
|
+
// No governance interventions — just show bubbles
|
|
4140
|
+
renderChoiceBubbles(reactions);
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
// Hide empty state
|
|
4145
|
+
var emptyEl = document.getElementById('choice-empty');
|
|
4146
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
4147
|
+
|
|
4148
|
+
// Build flow rows
|
|
4149
|
+
var maxCount = sortedFlows.length > 0 ? sortedFlows[0][1] : 1;
|
|
4150
|
+
var html = '<div class="decision-flow">';
|
|
4151
|
+
|
|
4152
|
+
sortedFlows.forEach(function(entry) {
|
|
4153
|
+
var parts = entry[0].split('|');
|
|
4154
|
+
var intended = parts[0], status = parts[1], actual = parts[2];
|
|
4155
|
+
var count = entry[1];
|
|
4156
|
+
var barWidth = Math.max(20, (count / maxCount) * 100);
|
|
4157
|
+
var arrowCls = status === 'BLOCK' ? 'blocked' : status === 'PENALIZE' ? 'penalized' : status === 'MODIFY' ? 'modified' : status === 'REWARD' ? 'rewarded' : 'allowed';
|
|
4158
|
+
var arrowChar = status === 'BLOCK' ? '✖' : status === 'PENALIZE' ? '⚠' : status === 'MODIFY' ? '➔' : status === 'REWARD' ? '★' : '→';
|
|
4159
|
+
var actualCls = 'actual-' + (status === 'BLOCK' ? 'block' : status === 'PENALIZE' ? 'penalize' : status === 'MODIFY' ? 'modify' : status === 'REWARD' ? 'reward' : 'allow');
|
|
4160
|
+
|
|
4161
|
+
html += '<div class="flow-row">' +
|
|
4162
|
+
'<div class="flow-intended"><span class="flow-label">' + intended + '</span><div class="flow-bar intended" style="width:' + barWidth + '%"><span class="flow-count">' + count + '</span></div></div>' +
|
|
4163
|
+
'<div class="flow-arrow ' + arrowCls + '">' + arrowChar + '</div>' +
|
|
4164
|
+
'<div class="flow-actual"><div class="flow-bar ' + actualCls + '" style="width:' + barWidth + '%"><span class="flow-count">' + count + '</span></div><span class="flow-label">' + actual + '</span></div>' +
|
|
4165
|
+
'</div>';
|
|
4166
|
+
});
|
|
4167
|
+
|
|
4168
|
+
html += '<div class="flow-legend">' +
|
|
4169
|
+
'<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#4ade80"></span> proceeded</div>' +
|
|
4170
|
+
'<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#fbbf24"></span> adjusted</div>' +
|
|
4171
|
+
'<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#f87171"></span> stopped</div>' +
|
|
4172
|
+
'<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#4ade80;border:1px solid #22c55e"></span> rewarded</div>' +
|
|
4173
|
+
'<div class="flow-legend-item"><span class="flow-legend-dot" style="background:#fb923c"></span> penalized</div>' +
|
|
4174
|
+
'</div>';
|
|
4175
|
+
html += '</div>';
|
|
4176
|
+
|
|
4177
|
+
landscapeEl.innerHTML = html;
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
// ── Choice Stream ──
|
|
4181
|
+
function updateChoiceStream(reactions, round) {
|
|
4182
|
+
var streamEl = document.getElementById('choice-stream');
|
|
4183
|
+
var countEl = document.getElementById('stream-count');
|
|
4184
|
+
if (!streamEl) return;
|
|
4185
|
+
|
|
4186
|
+
// Take latest 8 reactions to show
|
|
4187
|
+
var recent = reactions.slice(-8).reverse();
|
|
4188
|
+
var html = recent.map(function(r) {
|
|
4189
|
+
var status = r.verdict ? r.verdict.status : 'ALLOW';
|
|
4190
|
+
var badge = '';
|
|
4191
|
+
if (status === 'PENALIZE') badge = '<span class="incentive-badge penalize">⚠</span> ';
|
|
4192
|
+
else if (status === 'REWARD') badge = '<span class="incentive-badge reward">★</span> ';
|
|
4193
|
+
else if (status === 'BLOCK') badge = '<span style="color:#f87171;font-size:9px">✖</span> ';
|
|
4194
|
+
var frozenInfo = r.verdict && r.verdict.agentState && r.verdict.agentState.cooldown > 0
|
|
4195
|
+
? ' <span style="color:#818cf8;font-size:9px">(frozen)</span>' : '';
|
|
4196
|
+
return '<div class="stream-item">' +
|
|
4197
|
+
badge +
|
|
4198
|
+
'<span class="stream-agent">' + r.stakeholder_id + '</span>' +
|
|
4199
|
+
'<span class="stream-action">chose to <strong>' + normalizeAction(r.reaction) + '</strong>' + frozenInfo + '</span>' +
|
|
4200
|
+
'<span class="stream-round">R' + round + '</span>' +
|
|
4201
|
+
'</div>';
|
|
4202
|
+
}).join('');
|
|
4203
|
+
|
|
4204
|
+
streamEl.innerHTML = html;
|
|
4205
|
+
if (countEl) countEl.textContent = totalActions + ' total';
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
// ── Snapshot for drift ──
|
|
4209
|
+
function takeRoundSnapshot(reactions, round) {
|
|
4210
|
+
var choices = {};
|
|
4211
|
+
reactions.forEach(function(r) {
|
|
4212
|
+
var action = normalizeAction(r.reaction);
|
|
4213
|
+
choices[action] = (choices[action] || 0) + 1;
|
|
4214
|
+
});
|
|
4215
|
+
|
|
4216
|
+
var snapshot = { round: round, choices: choices, total: reactions.length };
|
|
4217
|
+
roundSnapshots.push(snapshot);
|
|
4218
|
+
if (!firstRoundSnapshot) firstRoundSnapshot = snapshot;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
// ── Behavioral Drift (post-run) ──
|
|
4222
|
+
function renderBehavioralDrift() {
|
|
4223
|
+
var driftSection = document.getElementById('deck-drift-section');
|
|
4224
|
+
var verdictEl = document.getElementById('drift-verdict');
|
|
4225
|
+
var comparisonEl = document.getElementById('drift-comparison');
|
|
4226
|
+
if (!driftSection || !verdictEl || !comparisonEl) return;
|
|
4227
|
+
if (roundSnapshots.length < 2) return;
|
|
4228
|
+
|
|
4229
|
+
var first = firstRoundSnapshot;
|
|
4230
|
+
var last = roundSnapshots[roundSnapshots.length - 1];
|
|
4231
|
+
driftSection.style.display = '';
|
|
4232
|
+
|
|
4233
|
+
// Collect all actions across both snapshots
|
|
4234
|
+
var allActions = {};
|
|
4235
|
+
Object.keys(first.choices).forEach(function(a) { allActions[a] = true; });
|
|
4236
|
+
Object.keys(last.choices).forEach(function(a) { allActions[a] = true; });
|
|
4237
|
+
var actions = Object.keys(allActions).sort(function(a, b) {
|
|
4238
|
+
return (last.choices[b] || 0) - (last.choices[a] || 0);
|
|
4239
|
+
});
|
|
4240
|
+
|
|
4241
|
+
// Generate verdict
|
|
4242
|
+
var biggestGain = { action: '', delta: 0 };
|
|
4243
|
+
var biggestLoss = { action: '', delta: 0 };
|
|
4244
|
+
actions.forEach(function(a) {
|
|
4245
|
+
var earlyPct = first.total > 0 ? (first.choices[a] || 0) / first.total : 0;
|
|
4246
|
+
var latePct = last.total > 0 ? (last.choices[a] || 0) / last.total : 0;
|
|
4247
|
+
var delta = latePct - earlyPct;
|
|
4248
|
+
if (delta > biggestGain.delta) { biggestGain = { action: a, delta: delta }; }
|
|
4249
|
+
if (delta < biggestLoss.delta) { biggestLoss = { action: a, delta: delta }; }
|
|
4250
|
+
});
|
|
4251
|
+
|
|
4252
|
+
var verdictParts = [];
|
|
4253
|
+
if (biggestGain.action && biggestGain.delta > 0.05) {
|
|
4254
|
+
verdictParts.push('Agents shifted toward <strong>' + biggestGain.action + '</strong> (+' + Math.round(biggestGain.delta * 100) + '%)');
|
|
4255
|
+
}
|
|
4256
|
+
if (biggestLoss.action && biggestLoss.delta < -0.05) {
|
|
4257
|
+
verdictParts.push('abandoned <strong>' + biggestLoss.action + '</strong> (' + Math.round(biggestLoss.delta * 100) + '%)');
|
|
4258
|
+
}
|
|
4259
|
+
verdictEl.innerHTML = verdictParts.length > 0
|
|
4260
|
+
? verdictParts.join(', ')
|
|
4261
|
+
: 'Agent behavior remained relatively stable across rounds.';
|
|
4262
|
+
|
|
4263
|
+
// Render drift bars
|
|
4264
|
+
var html = '<div class="drift-header-row"><span>Choice</span><span>Round 1</span><span></span><span>Final Round</span></div>';
|
|
4265
|
+
actions.slice(0, 8).forEach(function(action) {
|
|
4266
|
+
var earlyCount = first.choices[action] || 0;
|
|
4267
|
+
var lateCount = last.choices[action] || 0;
|
|
4268
|
+
var earlyPct = first.total > 0 ? Math.round(earlyCount / first.total * 100) : 0;
|
|
4269
|
+
var latePct = last.total > 0 ? Math.round(lateCount / last.total * 100) : 0;
|
|
4270
|
+
var delta = latePct - earlyPct;
|
|
4271
|
+
var deltaCls = delta > 2 ? 'up' : delta < -2 ? 'down' : 'flat';
|
|
4272
|
+
var deltaStr = delta > 0 ? '+' + delta + '%' : delta + '%';
|
|
4273
|
+
if (Math.abs(delta) <= 2) deltaStr = '—';
|
|
4274
|
+
var color = actionColor(action);
|
|
4275
|
+
|
|
4276
|
+
html += '<div class="drift-row">' +
|
|
4277
|
+
'<div class="drift-action">' + action + '</div>' +
|
|
4278
|
+
'<div class="drift-bar-container"><div class="drift-bar early" style="width:' + Math.max(2, earlyPct) + '%;background:' + color + ';opacity:0.5">' + earlyPct + '%</div></div>' +
|
|
4279
|
+
'<div class="drift-delta ' + deltaCls + '">' + deltaStr + '</div>' +
|
|
4280
|
+
'<div class="drift-bar-container"><div class="drift-bar late" style="width:' + Math.max(2, latePct) + '%;background:' + color + '">' + latePct + '%</div></div>' +
|
|
4281
|
+
'</div>';
|
|
4282
|
+
});
|
|
4283
|
+
|
|
4284
|
+
comparisonEl.innerHTML = html;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
// ── What-If counterfactual ──
|
|
4288
|
+
function renderWhatIf(result) {
|
|
4289
|
+
var section = document.getElementById('deck-whatif-section');
|
|
4290
|
+
var contentEl = document.getElementById('whatif-content');
|
|
4291
|
+
if (!section || !contentEl || !result || !result.governed || !result.baseline) return;
|
|
4292
|
+
|
|
4293
|
+
section.style.display = '';
|
|
4294
|
+
var govCollapse = Math.round(result.governed.metrics.collapseProbability * 100);
|
|
4295
|
+
var baseCollapse = Math.round(result.baseline.metrics.collapseProbability * 100);
|
|
4296
|
+
var govStability = Math.round(result.governed.metrics.stabilityScore * 100);
|
|
4297
|
+
var baseStability = Math.round(result.baseline.metrics.stabilityScore * 100);
|
|
4298
|
+
|
|
4299
|
+
contentEl.innerHTML =
|
|
4300
|
+
'<div class="whatif-stat"><span class="whatif-num bad">' + baseCollapse + '%</span><span class="whatif-label">collapse probability <em>without</em> your rules</span></div>' +
|
|
4301
|
+
'<div class="whatif-stat"><span class="whatif-num good">' + govCollapse + '%</span><span class="whatif-label">collapse probability <em>with</em> your rules</span></div>' +
|
|
4302
|
+
'<div class="whatif-compare">Stability went from ' + baseStability + '% to ' + govStability + '%. ' +
|
|
4303
|
+
(govStability > baseStability ? 'Your rules made the system ' + (govStability - baseStability) + ' points more stable.' : 'Agents found alternative strategies within your constraints.') +
|
|
4304
|
+
'</div>';
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
// ── Master update ──
|
|
4308
|
+
function updateAllPanels() {
|
|
4309
|
+
// Observation Deck updates are driven by renderChoiceBubbles/renderDecisionFlow
|
|
4310
|
+
// called directly from handleEvent — this function exists for compatibility
|
|
4311
|
+
}
|
|
4312
|
+
|
|
2610
4313
|
function handleEvent(event) {
|
|
2611
4314
|
if (event.type === 'meta') {
|
|
2612
4315
|
statusEl.className = 'status live';
|
|
2613
4316
|
statusEl.textContent = 'LIVE';
|
|
2614
4317
|
// Show simulation source
|
|
2615
|
-
const src = event.source || '
|
|
2616
|
-
if (src !== 'nv-sim') {
|
|
4318
|
+
const src = event.source || 'governance';
|
|
4319
|
+
if (src !== 'nv-sim' && src !== 'governance') {
|
|
2617
4320
|
traceSourceEl.textContent = '● ' + src.toUpperCase() + ' (LIVE)';
|
|
2618
4321
|
traceSourceEl.style.color = '#4ade80';
|
|
2619
|
-
engineStatusEl.textContent = 'Streaming from ' + src;
|
|
2620
|
-
engineStatusEl.style.color = '#4ade80';
|
|
2621
4322
|
} else {
|
|
2622
4323
|
traceSourceEl.textContent = '';
|
|
2623
|
-
engineStatusEl.textContent = '';
|
|
2624
4324
|
}
|
|
2625
4325
|
addLog('Simulation started: ' + event.agents.length + ' agents, ' + event.totalRounds + ' rounds' + (src !== 'nv-sim' ? ' [source: ' + src + ']' : ''));
|
|
2626
4326
|
resetShiftTracker();
|
|
4327
|
+
// Reset Observation Deck state
|
|
4328
|
+
roundSnapshots = [];
|
|
4329
|
+
firstRoundSnapshot = null;
|
|
4330
|
+
var deckStatusEl = document.getElementById('deck-status');
|
|
4331
|
+
if (deckStatusEl) { deckStatusEl.textContent = 'LIVE'; deckStatusEl.className = 'deck-status live'; }
|
|
4332
|
+
var driftSec = document.getElementById('deck-drift-section');
|
|
4333
|
+
if (driftSec) driftSec.style.display = 'none';
|
|
4334
|
+
var whatifSec = document.getElementById('deck-whatif-section');
|
|
4335
|
+
if (whatifSec) whatifSec.style.display = 'none';
|
|
4336
|
+
var choiceEmpty = document.getElementById('choice-empty');
|
|
4337
|
+
if (choiceEmpty) choiceEmpty.style.display = '';
|
|
4338
|
+
var roundLabel = document.getElementById('choice-round-label');
|
|
4339
|
+
if (roundLabel) roundLabel.style.display = 'none';
|
|
4340
|
+
var streamEl = document.getElementById('choice-stream');
|
|
4341
|
+
if (streamEl) streamEl.innerHTML = '<div class="stream-empty">Agent decisions will stream here in real time</div>';
|
|
4342
|
+
// Reset incentive tracker
|
|
4343
|
+
incentiveLog = [];
|
|
4344
|
+
var incentiveSec = document.getElementById('deck-incentive-section');
|
|
4345
|
+
if (incentiveSec) incentiveSec.style.display = 'none';
|
|
2627
4346
|
// Store narrative events by round for trace rendering
|
|
2628
4347
|
narrativeEventsByRound = {};
|
|
2629
4348
|
(event.narrativeEvents || []).forEach(function(ev) {
|
|
@@ -2689,44 +4408,78 @@ function handleEvent(event) {
|
|
|
2689
4408
|
'<div class="rule-impact" data-impact-id="' + g.id + '"></div>' +
|
|
2690
4409
|
'</div>';
|
|
2691
4410
|
}).join('');
|
|
2692
|
-
initChart();
|
|
2693
4411
|
}
|
|
2694
4412
|
|
|
2695
4413
|
if (event.type === 'round') {
|
|
2696
|
-
|
|
2697
|
-
chartLabels.push('R' + event.round);
|
|
2698
|
-
baselineImpacts.push(event.avgImpact);
|
|
2699
|
-
} else {
|
|
2700
|
-
governedImpacts.push(event.avgImpact);
|
|
2701
|
-
}
|
|
2702
|
-
if (chart) chart.update();
|
|
4414
|
+
const isBridge = event.reactions && event.reactions[0] && event.reactions[0].trigger === 'bridge';
|
|
2703
4415
|
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
document.getElementById('m-volatility').parentElement.className = 'metric-box ' + (event.maxVolatility > 0.6 ? 'bad' : event.maxVolatility > 0.4 ? 'warn' : 'good');
|
|
4416
|
+
// Record behavior data (no governance display)
|
|
4417
|
+
recordBehavior(event.reactions, event.round);
|
|
4418
|
+
renderAgents(event.reactions); // hidden, kept for audit
|
|
2708
4419
|
|
|
4420
|
+
// Track totals
|
|
4421
|
+
totalActions += event.reactions ? event.reactions.length : 0;
|
|
2709
4422
|
totalInterventions += event.interventionCount;
|
|
4423
|
+
latestVolatility = event.maxVolatility || 0;
|
|
4424
|
+
|
|
4425
|
+
// Update hidden data elements for audit
|
|
4426
|
+
if (isBridge) {
|
|
4427
|
+
document.getElementById('m-round').textContent = event.round + ' evals';
|
|
4428
|
+
} else {
|
|
4429
|
+
document.getElementById('m-round').textContent = event.round + '/' + event.totalRounds;
|
|
4430
|
+
}
|
|
4431
|
+
document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
|
|
2710
4432
|
document.getElementById('m-interventions').textContent = totalInterventions;
|
|
2711
4433
|
|
|
2712
|
-
// Track system shifts
|
|
4434
|
+
// Track system shifts (data collection)
|
|
2713
4435
|
trackShift(event);
|
|
2714
4436
|
|
|
2715
|
-
//
|
|
4437
|
+
// ── Observation Deck: live updates ──
|
|
4438
|
+
// Choice bubbles or decision flow (shows what agents chose and how it flowed)
|
|
4439
|
+
if (event.reactions && event.reactions.length > 0) {
|
|
4440
|
+
renderDecisionFlow(event.reactions);
|
|
4441
|
+
updateChoiceStream(event.reactions, event.round);
|
|
4442
|
+
takeRoundSnapshot(event.reactions, event.round);
|
|
4443
|
+
|
|
4444
|
+
// Track incentives (reward/penalize) from governance engine
|
|
4445
|
+
event.reactions.forEach(function(r) { trackIncentive(r, event.round); });
|
|
4446
|
+
|
|
4447
|
+
// Update round label
|
|
4448
|
+
var roundLabelEl = document.getElementById('choice-round-label');
|
|
4449
|
+
var roundNumEl = document.getElementById('choice-round-num');
|
|
4450
|
+
if (roundLabelEl) roundLabelEl.style.display = '';
|
|
4451
|
+
if (roundNumEl) roundNumEl.textContent = event.round;
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
// Add trace entry for audit
|
|
2716
4455
|
addTraceRound(event);
|
|
2717
4456
|
}
|
|
2718
4457
|
|
|
4458
|
+
// Bridge metrics — updates stability from external /api/evaluate calls
|
|
4459
|
+
if (event.type === 'bridge_metrics') {
|
|
4460
|
+
latestStability = event.stability;
|
|
4461
|
+
document.getElementById('m-stability').textContent = (event.stability * 100).toFixed(0) + '%';
|
|
4462
|
+
updateAllPanels();
|
|
4463
|
+
}
|
|
4464
|
+
|
|
2719
4465
|
if (event.type === 'complete') {
|
|
2720
4466
|
statusEl.className = 'status complete';
|
|
2721
4467
|
statusEl.textContent = 'COMPLETE';
|
|
2722
4468
|
const r = event.result;
|
|
2723
4469
|
if (r.governed) {
|
|
4470
|
+
latestStability = r.governed.metrics.stabilityScore;
|
|
4471
|
+
latestVolatility = r.governed.metrics.maxVolatility || latestVolatility;
|
|
2724
4472
|
document.getElementById('m-stability').textContent = (r.governed.metrics.stabilityScore * 100).toFixed(0) + '%';
|
|
2725
|
-
|
|
2726
|
-
addLog('Complete. Governance effectiveness: ' + (r.comparison.governanceEffectiveness * 100).toFixed(0) + '%');
|
|
4473
|
+
addLog('Simulation complete');
|
|
2727
4474
|
renderSystemShift(r);
|
|
2728
4475
|
renderRuleImpacts(r);
|
|
2729
4476
|
renderEnforcementClassification(r.enforcementClassification || []);
|
|
4477
|
+
|
|
4478
|
+
// Observation Deck: post-run views
|
|
4479
|
+
var deckStatusEl2 = document.getElementById('deck-status');
|
|
4480
|
+
if (deckStatusEl2) { deckStatusEl2.textContent = 'Complete'; deckStatusEl2.className = 'deck-status complete'; }
|
|
4481
|
+
renderBehavioralDrift();
|
|
4482
|
+
renderWhatIf(r);
|
|
2730
4483
|
lastSimResult = {
|
|
2731
4484
|
stability: r.governed.metrics.stabilityScore,
|
|
2732
4485
|
volatility: r.governed.metrics.maxVolatility,
|
|
@@ -2735,7 +4488,88 @@ function handleEvent(event) {
|
|
|
2735
4488
|
};
|
|
2736
4489
|
}
|
|
2737
4490
|
runBtn.disabled = false;
|
|
2738
|
-
runBtn.textContent = 'Run
|
|
4491
|
+
runBtn.textContent = 'Run Governance';
|
|
4492
|
+
|
|
4493
|
+
// Generate post-run governance suggestions
|
|
4494
|
+
generateGovernanceSuggestions(r);
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
// ============================================
|
|
4499
|
+
// POST-RUN GOVERNANCE SUGGESTIONS
|
|
4500
|
+
// ============================================
|
|
4501
|
+
function generateGovernanceSuggestions(result) {
|
|
4502
|
+
var suggestionsSection = document.getElementById('governance-suggestions-section');
|
|
4503
|
+
var suggestionsEl = document.getElementById('governance-suggestions');
|
|
4504
|
+
if (!suggestionsSection || !suggestionsEl) return;
|
|
4505
|
+
|
|
4506
|
+
var suggestions = [];
|
|
4507
|
+
var thesis = currentThesis || '';
|
|
4508
|
+
|
|
4509
|
+
if (result && result.governed && result.baseline) {
|
|
4510
|
+
var gov = result.governed.metrics;
|
|
4511
|
+
var base = result.baseline.metrics;
|
|
4512
|
+
var effectiveness = result.comparison ? result.comparison.governanceEffectiveness : 0;
|
|
4513
|
+
|
|
4514
|
+
// Analyze what happened and suggest improvements
|
|
4515
|
+
if (gov.collapseProbability > 0.3) {
|
|
4516
|
+
suggestions.push({
|
|
4517
|
+
type: 'warning',
|
|
4518
|
+
text: 'Collapse probability is ' + (gov.collapseProbability * 100).toFixed(0) + '% even with governance. Consider adding stricter gates or lower thresholds.',
|
|
4519
|
+
});
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
if (effectiveness < 0.3) {
|
|
4523
|
+
suggestions.push({
|
|
4524
|
+
type: 'warning',
|
|
4525
|
+
text: 'Governance effectiveness is low (' + (effectiveness * 100).toFixed(0) + '%). Your rules may be too loose or not matching agent behaviors. Consider tightening intent patterns.',
|
|
4526
|
+
});
|
|
4527
|
+
} else if (effectiveness > 0.8) {
|
|
4528
|
+
suggestions.push({
|
|
4529
|
+
type: 'success',
|
|
4530
|
+
text: 'Strong governance effectiveness (' + (effectiveness * 100).toFixed(0) + '%). Rules are actively shaping behavior.',
|
|
4531
|
+
});
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
if (gov.stabilityScore > base.stabilityScore) {
|
|
4535
|
+
var delta = ((gov.stabilityScore - base.stabilityScore) * 100).toFixed(0);
|
|
4536
|
+
suggestions.push({
|
|
4537
|
+
type: 'success',
|
|
4538
|
+
text: 'Governance improved stability by ' + delta + ' points (from ' + (base.stabilityScore * 100).toFixed(0) + '% to ' + (gov.stabilityScore * 100).toFixed(0) + '%).',
|
|
4539
|
+
});
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4542
|
+
// Check which rules never fired
|
|
4543
|
+
var unfiredRules = [];
|
|
4544
|
+
Object.keys(ruleImpactTracker).forEach(function(ruleId) {
|
|
4545
|
+
if (ruleImpactTracker[ruleId].blocks === 0) {
|
|
4546
|
+
unfiredRules.push(ruleImpactTracker[ruleId].label);
|
|
4547
|
+
}
|
|
4548
|
+
});
|
|
4549
|
+
if (unfiredRules.length > 0) {
|
|
4550
|
+
suggestions.push({
|
|
4551
|
+
type: 'info',
|
|
4552
|
+
text: unfiredRules.length + ' rule' + (unfiredRules.length > 1 ? 's' : '') + ' never triggered: ' + unfiredRules.slice(0, 3).join(', ') + (unfiredRules.length > 3 ? '...' : '') + '. Agents may already comply, or thresholds may be too high.',
|
|
4553
|
+
});
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
// Thesis-aware suggestion
|
|
4557
|
+
if (thesis) {
|
|
4558
|
+
suggestions.push({
|
|
4559
|
+
type: 'thesis',
|
|
4560
|
+
text: 'Your goal: "' + thesis + '". ' + (effectiveness > 0.5 ? 'Current rules are aligned with this goal.' : 'Consider adjusting rules to better match this intent.'),
|
|
4561
|
+
});
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
if (suggestions.length > 0) {
|
|
4566
|
+
suggestionsSection.style.display = '';
|
|
4567
|
+
var colorMap = { warning: 'var(--yellow)', success: 'var(--green)', info: 'var(--accent)', thesis: '#818cf8' };
|
|
4568
|
+
suggestionsEl.innerHTML = suggestions.map(function(s) {
|
|
4569
|
+
return '<div class="suggestion-item" style="border-left:3px solid ' + (colorMap[s.type] || 'var(--text-muted)') + '">' +
|
|
4570
|
+
'<div class="suggestion-text">' + s.text + '</div>' +
|
|
4571
|
+
'</div>';
|
|
4572
|
+
}).join('');
|
|
2739
4573
|
}
|
|
2740
4574
|
}
|
|
2741
4575
|
|
|
@@ -2805,20 +4639,37 @@ const ssImpactsEl = document.getElementById('ss-impacts');
|
|
|
2805
4639
|
const ssNarrativeEl = document.getElementById('ss-narrative');
|
|
2806
4640
|
|
|
2807
4641
|
// Raw detail toggle
|
|
2808
|
-
document.getElementById('ss-raw-toggle')
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
4642
|
+
var ssRawToggle = document.getElementById('ss-raw-toggle');
|
|
4643
|
+
if (ssRawToggle) {
|
|
4644
|
+
ssRawToggle.addEventListener('click', function() {
|
|
4645
|
+
this.classList.toggle('open');
|
|
4646
|
+
document.getElementById('ss-raw-detail').classList.toggle('open');
|
|
4647
|
+
});
|
|
4648
|
+
}
|
|
2812
4649
|
|
|
2813
4650
|
let shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
|
|
2814
4651
|
|
|
2815
4652
|
function resetShiftTracker() {
|
|
2816
4653
|
shiftTracker = { blocks: 0, total: 0, shifts: {}, patterns: [], baselineVol: 0, governedVol: 0, narrative: '', rawGoverned: [] };
|
|
2817
|
-
ssCard.classList.remove('visible');
|
|
4654
|
+
if (ssCard) ssCard.classList.remove('visible');
|
|
2818
4655
|
var rawToggle = document.getElementById('ss-raw-toggle');
|
|
2819
4656
|
var rawDetail = document.getElementById('ss-raw-detail');
|
|
2820
4657
|
if (rawToggle) rawToggle.classList.remove('open');
|
|
2821
4658
|
if (rawDetail) rawDetail.classList.remove('open');
|
|
4659
|
+
// Reset behavior tracking
|
|
4660
|
+
behaviorLog = [];
|
|
4661
|
+
totalActions = 0;
|
|
4662
|
+
totalInterventions = 0;
|
|
4663
|
+
latestStability = 0;
|
|
4664
|
+
latestVolatility = 0;
|
|
4665
|
+
// Reset visible panels
|
|
4666
|
+
if (outcomeStatementEl) outcomeStatementEl.innerHTML = '<span class="outcome-empty">Run a simulation or connect an agent to see outcomes</span>';
|
|
4667
|
+
if (confidenceGridEl) confidenceGridEl.style.display = 'none';
|
|
4668
|
+
if (outcomeContextEl) outcomeContextEl.textContent = '';
|
|
4669
|
+
if (behaviorShiftsEl) behaviorShiftsEl.innerHTML = '<div class="behavior-empty">Waiting for agent actions</div>';
|
|
4670
|
+
if (activityToggleEl) activityToggleEl.style.display = 'none';
|
|
4671
|
+
if (activityTimelineEl) { activityTimelineEl.innerHTML = ''; activityTimelineEl.classList.remove('open'); }
|
|
4672
|
+
if (whyContentEl) whyContentEl.innerHTML = '<div class="why-empty">Causation analysis appears after agent actions are evaluated</div>';
|
|
2822
4673
|
}
|
|
2823
4674
|
|
|
2824
4675
|
function trackShift(event) {
|
|
@@ -2861,6 +4712,7 @@ function trackShift(event) {
|
|
|
2861
4712
|
|
|
2862
4713
|
function renderSystemShift(result) {
|
|
2863
4714
|
if (shiftTracker.blocks === 0) return;
|
|
4715
|
+
if (!ssCard) return; // System shift card removed from visible UI
|
|
2864
4716
|
|
|
2865
4717
|
var adaptRate = shiftTracker.total > 0 ? Math.round((shiftTracker.blocks / shiftTracker.total) * 100) : 0;
|
|
2866
4718
|
|
|
@@ -2992,11 +4844,11 @@ cancelSaveBtn.addEventListener('click', () => {
|
|
|
2992
4844
|
|
|
2993
4845
|
confirmSaveBtn.addEventListener('click', async () => {
|
|
2994
4846
|
const name = variantNameInput.value.trim();
|
|
2995
|
-
if (!name
|
|
4847
|
+
if (!name) return;
|
|
2996
4848
|
|
|
2997
4849
|
// Gather current state overrides
|
|
2998
4850
|
const stateOverrides = {};
|
|
2999
|
-
if (currentWorld.stateVariables) {
|
|
4851
|
+
if (currentWorld && currentWorld.stateVariables) {
|
|
3000
4852
|
currentWorld.stateVariables.forEach(sv => {
|
|
3001
4853
|
const el = document.getElementById('sv-' + sv.id);
|
|
3002
4854
|
if (!el) return;
|
|
@@ -3006,13 +4858,17 @@ confirmSaveBtn.addEventListener('click', async () => {
|
|
|
3006
4858
|
});
|
|
3007
4859
|
}
|
|
3008
4860
|
|
|
4861
|
+
// Save as SimulationRules (complete snapshot)
|
|
3009
4862
|
const payload = {
|
|
3010
4863
|
name,
|
|
3011
4864
|
description: variantDescInput.value.trim(),
|
|
3012
|
-
|
|
4865
|
+
thesis: currentThesis,
|
|
4866
|
+
bridgeId: connectedBridge ? connectedBridge.id : undefined,
|
|
4867
|
+
rules: currentRules.map(function(r) {
|
|
4868
|
+
return { id: r.id, description: r.description, enforcement: r.enforcement, intent_patterns: r.intent_patterns || [] };
|
|
4869
|
+
}),
|
|
4870
|
+
worldDefinition: currentWorld || undefined,
|
|
3013
4871
|
stateOverrides,
|
|
3014
|
-
events: injectedEvents.slice(),
|
|
3015
|
-
rounds: parseInt(roundsSlider.value),
|
|
3016
4872
|
lastResult: lastSimResult,
|
|
3017
4873
|
};
|
|
3018
4874
|
|
|
@@ -3045,18 +4901,21 @@ async function loadVariants() {
|
|
|
3045
4901
|
|
|
3046
4902
|
function renderVariants(variants) {
|
|
3047
4903
|
if (variants.length === 0) {
|
|
3048
|
-
variantListEl.innerHTML = '<div style="font-size:11px;color
|
|
4904
|
+
variantListEl.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No saved rules yet</div>';
|
|
3049
4905
|
return;
|
|
3050
4906
|
}
|
|
3051
4907
|
variantListEl.innerHTML = variants.map(v => {
|
|
3052
4908
|
const resultHtml = v.lastResult
|
|
3053
4909
|
? '<span class="vresult">Stability: ' + (v.lastResult.stability * 100).toFixed(0) + '% | Effectiveness: ' + (v.lastResult.governanceEffectiveness * 100).toFixed(0) + '%</span>'
|
|
3054
|
-
: '<span style="color
|
|
4910
|
+
: '<span style="color:var(--text-faint)">Not yet run</span>';
|
|
4911
|
+
const rulesCount = v.rules ? v.rules.length + ' rules' : (v.events ? v.events.length + ' events' : '');
|
|
4912
|
+
const bridgeLabel = v.bridgeId ? '<span class="vbase">' + v.bridgeId + '</span>' : (v.baseWorld ? '<span class="vbase">' + v.baseWorld + '</span>' : '');
|
|
3055
4913
|
return '<div class="variant-card" data-vid="' + v.id + '">' +
|
|
3056
4914
|
'<div class="vname">' + v.name + '</div>' +
|
|
4915
|
+
(v.thesis ? '<div class="vdesc" style="font-style:italic">' + v.thesis + '</div>' : '') +
|
|
3057
4916
|
(v.description ? '<div class="vdesc">' + v.description + '</div>' : '') +
|
|
3058
|
-
|
|
3059
|
-
'<div class="vmeta">' + resultHtml + ' | ' +
|
|
4917
|
+
bridgeLabel +
|
|
4918
|
+
'<div class="vmeta">' + resultHtml + (rulesCount ? ' | ' + rulesCount : '') + '</div>' +
|
|
3060
4919
|
'<span class="vdelete" data-vid="' + v.id + '">delete</span>' +
|
|
3061
4920
|
'</div>';
|
|
3062
4921
|
}).join('');
|
|
@@ -3087,38 +4946,49 @@ function renderVariants(variants) {
|
|
|
3087
4946
|
}
|
|
3088
4947
|
|
|
3089
4948
|
function loadVariant(variant) {
|
|
3090
|
-
//
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
4949
|
+
// Handle new SimulationRules format
|
|
4950
|
+
if (variant.thesis !== undefined) {
|
|
4951
|
+
// Restore thesis
|
|
4952
|
+
currentThesis = variant.thesis;
|
|
4953
|
+
if (thesisInput) thesisInput.value = variant.thesis;
|
|
4954
|
+
|
|
4955
|
+
// Restore rules
|
|
4956
|
+
if (variant.rules && variant.rules.length > 0) {
|
|
4957
|
+
currentRules = variant.rules;
|
|
4958
|
+
// Re-apply rules to server
|
|
4959
|
+
fetch('/api/generate-rules', {
|
|
4960
|
+
method: 'POST',
|
|
4961
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4962
|
+
body: JSON.stringify({ thesis: variant.thesis }),
|
|
4963
|
+
}).catch(() => {});
|
|
4964
|
+
|
|
4965
|
+
// Show rules preview
|
|
4966
|
+
var iconMap = { block: '🔴', allow: '🟢', modify: '🔵', warn: '🟡' };
|
|
4967
|
+
generatedRulesPreviewEl.innerHTML = variant.rules.map(function(r) {
|
|
4968
|
+
var icon = iconMap[r.enforcement] || '🟢';
|
|
4969
|
+
return '<div class="parsed-rule enforcement-' + r.enforcement + '">' +
|
|
4970
|
+
'<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + (r.enforcement || 'rule') + '</span></div>' +
|
|
4971
|
+
'<div class="pr-desc">' + r.description + '</div>' +
|
|
4972
|
+
'</div>';
|
|
4973
|
+
}).join('');
|
|
3112
4974
|
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
4975
|
+
ruleGenStatusEl.textContent = variant.rules.length + ' rule(s) loaded from "' + variant.name + '".';
|
|
4976
|
+
ruleGenStatusEl.className = 'rule-status success';
|
|
4977
|
+
}
|
|
3116
4978
|
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
4979
|
+
// Restore world definition
|
|
4980
|
+
if (variant.worldDefinition) {
|
|
4981
|
+
currentWorld = variant.worldDefinition;
|
|
4982
|
+
renderStateVars();
|
|
4983
|
+
}
|
|
4984
|
+
} else {
|
|
4985
|
+
// Legacy WorldVariant format — try to load base world
|
|
4986
|
+
if (variant.baseWorld) {
|
|
4987
|
+
selectWorld(variant.baseWorld);
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
3120
4990
|
|
|
3121
|
-
addLog('Loaded
|
|
4991
|
+
addLog('Loaded rules: ' + variant.name, '');
|
|
3122
4992
|
}
|
|
3123
4993
|
|
|
3124
4994
|
// ============================================
|
|
@@ -3151,18 +5021,6 @@ function applyTheme(theme) {
|
|
|
3151
5021
|
themeToggleBtn.textContent = 'Light Mode';
|
|
3152
5022
|
}
|
|
3153
5023
|
localStorage.setItem('nv-theme', theme);
|
|
3154
|
-
// Update chart colors if chart exists
|
|
3155
|
-
if (chart && chart.options) {
|
|
3156
|
-
const gridColor = theme === 'light' ? '#d4d4d4' : '#2a2a2a';
|
|
3157
|
-
const tickColor = theme === 'light' ? '#666' : '#888';
|
|
3158
|
-
const legendColor = theme === 'light' ? '#444' : '#888';
|
|
3159
|
-
chart.options.scales.x.ticks.color = tickColor;
|
|
3160
|
-
chart.options.scales.x.grid.color = gridColor;
|
|
3161
|
-
chart.options.scales.y.ticks.color = tickColor;
|
|
3162
|
-
chart.options.scales.y.grid.color = gridColor;
|
|
3163
|
-
chart.options.plugins.legend.labels.color = legendColor;
|
|
3164
|
-
chart.update();
|
|
3165
|
-
}
|
|
3166
5024
|
}
|
|
3167
5025
|
themeToggleBtn.addEventListener('click', () => {
|
|
3168
5026
|
const current = document.body.classList.contains('light') ? 'light' : 'dark';
|
|
@@ -3173,129 +5031,15 @@ const savedTheme = localStorage.getItem('nv-theme');
|
|
|
3173
5031
|
if (savedTheme) applyTheme(savedTheme);
|
|
3174
5032
|
|
|
3175
5033
|
// ============================================
|
|
3176
|
-
//
|
|
5034
|
+
// (Old world source switching removed — replaced by thesis + bridge flow)
|
|
3177
5035
|
// ============================================
|
|
3178
|
-
let currentWorldSource = 'preset';
|
|
3179
|
-
const worldSourceTabs = document.querySelectorAll('.ws-tab');
|
|
3180
|
-
const sourcePresetPanel = document.getElementById('source-preset');
|
|
3181
|
-
const sourceCustomPanel = document.getElementById('source-custom');
|
|
3182
|
-
const sourceUploadPanel = document.getElementById('source-upload');
|
|
3183
|
-
|
|
3184
|
-
worldSourceTabs.forEach(tab => {
|
|
3185
|
-
tab.addEventListener('click', () => {
|
|
3186
|
-
const source = tab.dataset.source;
|
|
3187
|
-
if (source === currentWorldSource) return;
|
|
3188
|
-
|
|
3189
|
-
currentWorldSource = source;
|
|
3190
|
-
|
|
3191
|
-
// Update tab visuals
|
|
3192
|
-
worldSourceTabs.forEach(t => t.classList.remove('active'));
|
|
3193
|
-
tab.classList.add('active');
|
|
3194
|
-
tab.querySelector('input').checked = true;
|
|
3195
|
-
|
|
3196
|
-
// Show/hide panels
|
|
3197
|
-
sourcePresetPanel.style.display = source === 'preset' ? '' : 'none';
|
|
3198
|
-
sourceCustomPanel.style.display = source === 'custom' ? '' : 'none';
|
|
3199
|
-
sourceUploadPanel.style.display = source === 'upload' ? '' : 'none';
|
|
3200
|
-
});
|
|
3201
|
-
});
|
|
3202
|
-
|
|
3203
|
-
// Populate base world selector in custom rules panel
|
|
3204
|
-
function populateBaseWorldSelect() {
|
|
3205
|
-
const select = document.getElementById('custom-base-world');
|
|
3206
|
-
if (!select) return;
|
|
3207
|
-
worlds.forEach(w => {
|
|
3208
|
-
const opt = document.createElement('option');
|
|
3209
|
-
opt.value = w.id;
|
|
3210
|
-
opt.textContent = w.title;
|
|
3211
|
-
select.appendChild(opt);
|
|
3212
|
-
});
|
|
3213
|
-
}
|
|
3214
5036
|
|
|
3215
5037
|
// ============================================
|
|
3216
|
-
//
|
|
5038
|
+
// (Old world action bar removed — replaced by thesis + bridge flow)
|
|
3217
5039
|
// ============================================
|
|
3218
5040
|
|
|
3219
|
-
// + New World
|
|
3220
|
-
document.getElementById('new-world-btn').addEventListener('click', () => {
|
|
3221
|
-
// Switch to custom rules mode
|
|
3222
|
-
currentWorldSource = 'custom';
|
|
3223
|
-
worldSourceTabs.forEach(t => {
|
|
3224
|
-
t.classList.toggle('active', t.dataset.source === 'custom');
|
|
3225
|
-
t.querySelector('input').checked = t.dataset.source === 'custom';
|
|
3226
|
-
});
|
|
3227
|
-
sourcePresetPanel.style.display = 'none';
|
|
3228
|
-
sourceCustomPanel.style.display = '';
|
|
3229
|
-
sourceUploadPanel.style.display = 'none';
|
|
3230
|
-
|
|
3231
|
-
// Clear everything
|
|
3232
|
-
document.getElementById('custom-world-name').value = '';
|
|
3233
|
-
document.getElementById('custom-world-thesis').value = '';
|
|
3234
|
-
document.getElementById('rule-input').value = '';
|
|
3235
|
-
document.getElementById('parsed-rules').innerHTML = '';
|
|
3236
|
-
document.getElementById('rule-status').textContent = '';
|
|
3237
|
-
document.getElementById('rule-status').className = 'rule-status';
|
|
3238
|
-
document.getElementById('custom-base-world').value = '';
|
|
3239
|
-
|
|
3240
|
-
// Clear active rules server-side
|
|
3241
|
-
fetch('/api/clear-rules', { method: 'POST' });
|
|
3242
|
-
|
|
3243
|
-
// Reset right panel
|
|
3244
|
-
document.getElementById('active-invariants').innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No rules loaded. Define your world.</div>';
|
|
3245
|
-
});
|
|
3246
|
-
|
|
3247
|
-
// Clear Rules
|
|
3248
|
-
document.getElementById('clear-rules-btn').addEventListener('click', async () => {
|
|
3249
|
-
await fetch('/api/clear-rules', { method: 'POST' });
|
|
3250
|
-
|
|
3251
|
-
// Clear rule editor UI
|
|
3252
|
-
const ruleInput = document.getElementById('rule-input');
|
|
3253
|
-
if (ruleInput) ruleInput.value = '';
|
|
3254
|
-
const parsed = document.getElementById('parsed-rules');
|
|
3255
|
-
if (parsed) parsed.innerHTML = '';
|
|
3256
|
-
const status = document.getElementById('rule-status');
|
|
3257
|
-
if (status) { status.textContent = 'Rules cleared.'; status.className = 'rule-status success'; }
|
|
3258
|
-
|
|
3259
|
-
// Clear upload state
|
|
3260
|
-
const uploadStatus = document.getElementById('upload-status');
|
|
3261
|
-
if (uploadStatus) { uploadStatus.textContent = 'Rules cleared.'; uploadStatus.className = 'rule-status success'; }
|
|
3262
|
-
const loadedInfo = document.getElementById('loaded-world-info');
|
|
3263
|
-
if (loadedInfo) loadedInfo.style.display = 'none';
|
|
3264
|
-
});
|
|
3265
|
-
|
|
3266
|
-
// Load World File (switch to upload tab)
|
|
3267
|
-
document.getElementById('load-file-btn').addEventListener('click', () => {
|
|
3268
|
-
currentWorldSource = 'upload';
|
|
3269
|
-
worldSourceTabs.forEach(t => {
|
|
3270
|
-
t.classList.toggle('active', t.dataset.source === 'upload');
|
|
3271
|
-
t.querySelector('input').checked = t.dataset.source === 'upload';
|
|
3272
|
-
});
|
|
3273
|
-
sourcePresetPanel.style.display = 'none';
|
|
3274
|
-
sourceCustomPanel.style.display = 'none';
|
|
3275
|
-
sourceUploadPanel.style.display = '';
|
|
3276
|
-
});
|
|
3277
|
-
|
|
3278
|
-
// Save as World File (export)
|
|
3279
|
-
document.getElementById('export-world-btn').addEventListener('click', async () => {
|
|
3280
|
-
try {
|
|
3281
|
-
const resp = await fetch('/api/export-world');
|
|
3282
|
-
const data = await resp.json();
|
|
3283
|
-
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
3284
|
-
const url = URL.createObjectURL(blob);
|
|
3285
|
-
const a = document.createElement('a');
|
|
3286
|
-
a.href = url;
|
|
3287
|
-
a.download = (currentWorld ? currentWorld.id : 'custom') + '-world.json';
|
|
3288
|
-
document.body.appendChild(a);
|
|
3289
|
-
a.click();
|
|
3290
|
-
document.body.removeChild(a);
|
|
3291
|
-
URL.revokeObjectURL(url);
|
|
3292
|
-
} catch (err) {
|
|
3293
|
-
alert('Export failed: ' + err.message);
|
|
3294
|
-
}
|
|
3295
|
-
});
|
|
3296
|
-
|
|
3297
5041
|
// ============================================
|
|
3298
|
-
// WORLD FILE UPLOAD / PASTE
|
|
5042
|
+
// WORLD FILE UPLOAD / PASTE (simplified)
|
|
3299
5043
|
// ============================================
|
|
3300
5044
|
const uploadZone = document.getElementById('upload-zone');
|
|
3301
5045
|
const uploadFileInput = document.getElementById('upload-file-input');
|
|
@@ -3304,56 +5048,43 @@ const worldJsonInput = document.getElementById('world-json-input');
|
|
|
3304
5048
|
const loadWorldBtn = document.getElementById('load-world-btn');
|
|
3305
5049
|
const uploadStatusEl = document.getElementById('upload-status');
|
|
3306
5050
|
|
|
3307
|
-
|
|
3308
|
-
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
|
|
3309
|
-
uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
|
|
3310
|
-
uploadZone.addEventListener('drop', (e) => {
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
});
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadFileInput.click(); });
|
|
3319
|
-
uploadFileInput.addEventListener('change', () => {
|
|
3320
|
-
if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]);
|
|
3321
|
-
});
|
|
3322
|
-
|
|
3323
|
-
// Click zone to browse
|
|
3324
|
-
uploadZone.addEventListener('click', () => { uploadFileInput.click(); });
|
|
5051
|
+
if (uploadZone) {
|
|
5052
|
+
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
|
|
5053
|
+
uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
|
|
5054
|
+
uploadZone.addEventListener('drop', (e) => {
|
|
5055
|
+
e.preventDefault();
|
|
5056
|
+
uploadZone.classList.remove('dragover');
|
|
5057
|
+
const file = e.dataTransfer.files[0];
|
|
5058
|
+
if (file) readWorldFile(file);
|
|
5059
|
+
});
|
|
5060
|
+
uploadZone.addEventListener('click', () => { if (uploadFileInput) uploadFileInput.click(); });
|
|
5061
|
+
}
|
|
5062
|
+
if (uploadBrowseBtn) uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); if (uploadFileInput) uploadFileInput.click(); });
|
|
5063
|
+
if (uploadFileInput) uploadFileInput.addEventListener('change', () => { if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]); });
|
|
3325
5064
|
|
|
3326
5065
|
function readWorldFile(file) {
|
|
3327
5066
|
const reader = new FileReader();
|
|
3328
5067
|
reader.onload = (e) => {
|
|
3329
|
-
worldJsonInput.value = e.target.result;
|
|
3330
|
-
uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".';
|
|
3331
|
-
uploadStatusEl.className = 'rule-status success';
|
|
5068
|
+
if (worldJsonInput) worldJsonInput.value = e.target.result;
|
|
5069
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".'; uploadStatusEl.className = 'rule-status success'; }
|
|
3332
5070
|
};
|
|
3333
5071
|
reader.readAsText(file);
|
|
3334
5072
|
}
|
|
3335
5073
|
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
const jsonText = worldJsonInput.value.trim();
|
|
5074
|
+
if (loadWorldBtn) loadWorldBtn.addEventListener('click', async () => {
|
|
5075
|
+
const jsonText = worldJsonInput ? worldJsonInput.value.trim() : '';
|
|
3339
5076
|
if (!jsonText) {
|
|
3340
|
-
uploadStatusEl.textContent = 'Paste or upload a world file first.';
|
|
3341
|
-
uploadStatusEl.className = 'rule-status error';
|
|
5077
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = 'Paste or upload a world file first.'; uploadStatusEl.className = 'rule-status error'; }
|
|
3342
5078
|
return;
|
|
3343
5079
|
}
|
|
3344
5080
|
|
|
3345
5081
|
let worldData;
|
|
3346
|
-
try {
|
|
3347
|
-
|
|
3348
|
-
} catch (err) {
|
|
3349
|
-
uploadStatusEl.textContent = 'Invalid JSON: ' + err.message;
|
|
3350
|
-
uploadStatusEl.className = 'rule-status error';
|
|
5082
|
+
try { worldData = JSON.parse(jsonText); } catch (err) {
|
|
5083
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = 'Invalid JSON: ' + err.message; uploadStatusEl.className = 'rule-status error'; }
|
|
3351
5084
|
return;
|
|
3352
5085
|
}
|
|
3353
5086
|
|
|
3354
|
-
// Normalize: if the JSON is { world: {...} } or just {...}
|
|
3355
5087
|
const worldPayload = worldData.world || worldData;
|
|
3356
|
-
|
|
3357
5088
|
loadWorldBtn.textContent = 'Loading...';
|
|
3358
5089
|
loadWorldBtn.disabled = true;
|
|
3359
5090
|
|
|
@@ -3366,176 +5097,50 @@ loadWorldBtn.addEventListener('click', async () => {
|
|
|
3366
5097
|
const result = await resp.json();
|
|
3367
5098
|
|
|
3368
5099
|
if (result.error) {
|
|
3369
|
-
uploadStatusEl.textContent = result.error;
|
|
3370
|
-
uploadStatusEl.className = 'rule-status error';
|
|
5100
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = result.error; uploadStatusEl.className = 'rule-status error'; }
|
|
3371
5101
|
} else {
|
|
3372
|
-
uploadStatusEl.textContent = result.message;
|
|
3373
|
-
uploadStatusEl.className = 'rule-status success';
|
|
5102
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = result.message; uploadStatusEl.className = 'rule-status success'; }
|
|
3374
5103
|
|
|
3375
5104
|
// Show loaded world info
|
|
3376
|
-
|
|
3377
|
-
infoEl.style.display = '';
|
|
3378
|
-
document.getElementById('lw-name')
|
|
3379
|
-
|
|
3380
|
-
document.getElementById('lw-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
//
|
|
3386
|
-
|
|
5105
|
+
var infoEl = document.getElementById('loaded-world-info');
|
|
5106
|
+
if (infoEl) infoEl.style.display = '';
|
|
5107
|
+
var lwName = document.getElementById('lw-name');
|
|
5108
|
+
if (lwName) lwName.textContent = result.world.title;
|
|
5109
|
+
var lwThesis = document.getElementById('lw-thesis');
|
|
5110
|
+
if (lwThesis) lwThesis.textContent = '"' + result.world.thesis + '"';
|
|
5111
|
+
var lwStats = document.getElementById('lw-stats');
|
|
5112
|
+
if (lwStats) lwStats.textContent = result.world.invariants.length + ' invariants, ' + result.world.gates.length + ' gates, ' + result.rulesApplied + ' rules';
|
|
5113
|
+
|
|
5114
|
+
// Set thesis from loaded world
|
|
5115
|
+
if (result.world.thesis && thesisInput) {
|
|
5116
|
+
thesisInput.value = result.world.thesis;
|
|
5117
|
+
currentThesis = result.world.thesis;
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
// Set current world
|
|
5121
|
+
currentWorld = {
|
|
5122
|
+
id: 'loaded-world',
|
|
5123
|
+
title: result.world.title,
|
|
5124
|
+
thesis: result.world.thesis,
|
|
5125
|
+
stateVariables: result.world.stateVariables || [],
|
|
5126
|
+
invariants: result.world.invariants || [],
|
|
5127
|
+
gates: result.world.gates || [],
|
|
5128
|
+
};
|
|
5129
|
+
renderStateVars();
|
|
5130
|
+
|
|
5131
|
+
// Update active invariants (hidden, for audit)
|
|
5132
|
+
activeInvEl.innerHTML = (result.world.invariants || []).map(inv =>
|
|
3387
5133
|
'<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
|
|
3388
|
-
).join('') + result.world.gates.map(g =>
|
|
3389
|
-
'<div class="inv-item" style="color:' + (g.severity === 'critical' ? 'var(--red)' : 'var(--yellow)') + '">[' + g.id + '] ' + g.label + '</div>'
|
|
3390
5134
|
).join('');
|
|
3391
|
-
activeInvEl.innerHTML = invHtml || '<div style="font-size:11px;color:var(--text-muted)">No invariants defined</div>';
|
|
3392
|
-
|
|
3393
|
-
// Update state variables if present
|
|
3394
|
-
if (result.world.stateVariables && result.world.stateVariables.length > 0) {
|
|
3395
|
-
// Store as a pseudo-world so sliders render
|
|
3396
|
-
currentWorld = {
|
|
3397
|
-
id: 'custom-world',
|
|
3398
|
-
title: result.world.title,
|
|
3399
|
-
thesis: result.world.thesis,
|
|
3400
|
-
stateVariables: result.world.stateVariables,
|
|
3401
|
-
invariants: result.world.invariants,
|
|
3402
|
-
gates: result.world.gates,
|
|
3403
|
-
};
|
|
3404
|
-
selectWorld('custom-world');
|
|
3405
|
-
} else {
|
|
3406
|
-
// Just set current world reference
|
|
3407
|
-
currentWorld = {
|
|
3408
|
-
id: 'custom-world',
|
|
3409
|
-
title: result.world.title,
|
|
3410
|
-
thesis: result.world.thesis,
|
|
3411
|
-
stateVariables: [],
|
|
3412
|
-
invariants: result.world.invariants,
|
|
3413
|
-
gates: result.world.gates,
|
|
3414
|
-
};
|
|
3415
|
-
}
|
|
3416
5135
|
}
|
|
3417
5136
|
} catch (err) {
|
|
3418
|
-
uploadStatusEl.textContent = 'Error: ' + err.message;
|
|
3419
|
-
uploadStatusEl.className = 'rule-status error';
|
|
5137
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = 'Error: ' + err.message; uploadStatusEl.className = 'rule-status error'; }
|
|
3420
5138
|
}
|
|
3421
5139
|
|
|
3422
5140
|
loadWorldBtn.textContent = 'Load into Runtime';
|
|
3423
5141
|
loadWorldBtn.disabled = false;
|
|
3424
5142
|
});
|
|
3425
5143
|
|
|
3426
|
-
// ============================================
|
|
3427
|
-
// PLAIN-ENGLISH RULE EDITOR
|
|
3428
|
-
// ============================================
|
|
3429
|
-
const ruleInput = document.getElementById('rule-input');
|
|
3430
|
-
const parseRulesBtn = document.getElementById('parse-rules-btn');
|
|
3431
|
-
const parsedRulesEl = document.getElementById('parsed-rules');
|
|
3432
|
-
const ruleStatusEl = document.getElementById('rule-status');
|
|
3433
|
-
let parsedRuleData = [];
|
|
3434
|
-
|
|
3435
|
-
parseRulesBtn.addEventListener('click', async () => {
|
|
3436
|
-
const text = ruleInput.value.trim();
|
|
3437
|
-
if (!text) return;
|
|
3438
|
-
|
|
3439
|
-
parseRulesBtn.disabled = true;
|
|
3440
|
-
parseRulesBtn.textContent = 'Parsing...';
|
|
3441
|
-
ruleStatusEl.textContent = '';
|
|
3442
|
-
ruleStatusEl.className = 'rule-status';
|
|
3443
|
-
|
|
3444
|
-
try {
|
|
3445
|
-
const resp = await fetch('/api/parse-rules', {
|
|
3446
|
-
method: 'POST',
|
|
3447
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3448
|
-
body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'trading' }),
|
|
3449
|
-
});
|
|
3450
|
-
const data = await resp.json();
|
|
3451
|
-
|
|
3452
|
-
if (data.error) {
|
|
3453
|
-
ruleStatusEl.textContent = data.error;
|
|
3454
|
-
ruleStatusEl.className = 'rule-status error';
|
|
3455
|
-
parsedRulesEl.innerHTML = '';
|
|
3456
|
-
parsedRuleData = [];
|
|
3457
|
-
} else {
|
|
3458
|
-
parsedRuleData = data.rules || [];
|
|
3459
|
-
parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
|
|
3460
|
-
const enfType = r.enforcement || 'block';
|
|
3461
|
-
const iconMap = { block: '🔴', allow: '🟢', modify: '🔵', warn: '🟡', pause: '🟡' };
|
|
3462
|
-
const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
|
|
3463
|
-
const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
|
|
3464
|
-
const icon = iconMap[enfType] || '🟢';
|
|
3465
|
-
const label = labelMap[enfType] || 'Rule';
|
|
3466
|
-
const effect = effectMap[enfType] || 'Active';
|
|
3467
|
-
return '<div class="parsed-rule enforcement-' + enfType + '">' +
|
|
3468
|
-
'<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
|
|
3469
|
-
'<div class="pr-desc">' + r.description + '</div>' +
|
|
3470
|
-
'<div class="pr-patterns">' + effect + ' • Matches: ' + r.intent_patterns.join(', ') + '</div>' +
|
|
3471
|
-
'</div>';
|
|
3472
|
-
}).join('');
|
|
3473
|
-
|
|
3474
|
-
if (parsedRuleData.length > 0) {
|
|
3475
|
-
const btnLabel = currentWorldSource === 'custom' ? 'Generate World with ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') : 'Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation';
|
|
3476
|
-
parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">' + btnLabel + '</button>';
|
|
3477
|
-
document.getElementById('apply-rules-btn').addEventListener('click', async () => {
|
|
3478
|
-
try {
|
|
3479
|
-
// If in custom rules mode, use base world if selected
|
|
3480
|
-
let worldId = currentWorld ? currentWorld.id : 'trading';
|
|
3481
|
-
if (currentWorldSource === 'custom') {
|
|
3482
|
-
const baseWorld = document.getElementById('custom-base-world').value;
|
|
3483
|
-
if (baseWorld) worldId = baseWorld;
|
|
3484
|
-
}
|
|
3485
|
-
|
|
3486
|
-
const applyResp = await fetch('/api/apply-rules', {
|
|
3487
|
-
method: 'POST',
|
|
3488
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3489
|
-
body: JSON.stringify({ rules: parsedRuleData, worldId }),
|
|
3490
|
-
});
|
|
3491
|
-
const applyData = await applyResp.json();
|
|
3492
|
-
if (applyData.status === 'applied') {
|
|
3493
|
-
ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
|
|
3494
|
-
ruleStatusEl.className = 'rule-status success';
|
|
3495
|
-
|
|
3496
|
-
// Update right panel invariants with custom rules
|
|
3497
|
-
const customName = document.getElementById('custom-world-name');
|
|
3498
|
-
const worldName = (customName && customName.value) ? customName.value : 'Custom World';
|
|
3499
|
-
const customThesis = document.getElementById('custom-world-thesis');
|
|
3500
|
-
const thesis = (customThesis && customThesis.value) ? customThesis.value : 'User-defined governance rules';
|
|
3501
|
-
|
|
3502
|
-
// Show rules in active invariants panel
|
|
3503
|
-
activeInvEl.innerHTML = parsedRuleData.map(r => {
|
|
3504
|
-
const enfType = r.enforcement || 'block';
|
|
3505
|
-
const colorMap = { block: 'var(--red)', allow: 'var(--green)', modify: 'var(--blue)', warn: 'var(--yellow)', pause: 'var(--yellow)' };
|
|
3506
|
-
const color = colorMap[enfType] || 'var(--text-secondary)';
|
|
3507
|
-
return '<div class="inv-item" style="color:' + color + '">[' + r.id + '] ' + r.description + '</div>';
|
|
3508
|
-
}).join('');
|
|
3509
|
-
|
|
3510
|
-
// In custom mode, set a custom world reference
|
|
3511
|
-
if (currentWorldSource === 'custom') {
|
|
3512
|
-
const baseWorld = document.getElementById('custom-base-world').value;
|
|
3513
|
-
if (baseWorld) {
|
|
3514
|
-
selectWorld(baseWorld);
|
|
3515
|
-
} else {
|
|
3516
|
-
currentWorld = { id: 'custom-world', title: worldName, thesis, stateVariables: [], invariants: [], gates: [] };
|
|
3517
|
-
}
|
|
3518
|
-
document.getElementById('world-thesis').textContent = '"' + thesis + '"';
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
|
-
} catch (err) {
|
|
3522
|
-
ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
|
|
3523
|
-
ruleStatusEl.className = 'rule-status error';
|
|
3524
|
-
}
|
|
3525
|
-
});
|
|
3526
|
-
ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
|
|
3527
|
-
ruleStatusEl.className = 'rule-status success';
|
|
3528
|
-
}
|
|
3529
|
-
}
|
|
3530
|
-
} catch (err) {
|
|
3531
|
-
ruleStatusEl.textContent = 'Error: ' + err.message;
|
|
3532
|
-
ruleStatusEl.className = 'rule-status error';
|
|
3533
|
-
}
|
|
3534
|
-
|
|
3535
|
-
parseRulesBtn.disabled = false;
|
|
3536
|
-
parseRulesBtn.textContent = 'Parse Rules';
|
|
3537
|
-
});
|
|
3538
|
-
|
|
3539
5144
|
// ============================================
|
|
3540
5145
|
// SESSION TRACKING
|
|
3541
5146
|
// ============================================
|
|
@@ -3551,6 +5156,8 @@ async function pollSessionStats() {
|
|
|
3551
5156
|
if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
|
|
3552
5157
|
if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
|
|
3553
5158
|
if (el('s-allowed')) el('s-allowed').textContent = data.evaluations.allowed;
|
|
5159
|
+
if (el('s-rewarded')) el('s-rewarded').textContent = data.evaluations.rewarded || 0;
|
|
5160
|
+
if (el('s-penalized')) el('s-penalized').textContent = data.evaluations.penalized || 0;
|
|
3554
5161
|
if (el('s-agents')) {
|
|
3555
5162
|
el('s-agents').textContent = data.agents.length > 0
|
|
3556
5163
|
? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
|
|
@@ -3612,6 +5219,189 @@ async function saveExperiment() {
|
|
|
3612
5219
|
sessionPollInterval = setInterval(pollSessionStats, 2000);
|
|
3613
5220
|
pollSessionStats();
|
|
3614
5221
|
|
|
5222
|
+
// ============================================
|
|
5223
|
+
// AI API KEY MANAGEMENT
|
|
5224
|
+
// ============================================
|
|
5225
|
+
var aiKeyInput = document.getElementById('ai-key-input');
|
|
5226
|
+
var aiKeyStatus = document.getElementById('ai-key-status');
|
|
5227
|
+
var aiKeySaveBtn = document.getElementById('ai-key-save-btn');
|
|
5228
|
+
var aiKeyExtra = document.getElementById('ai-key-extra');
|
|
5229
|
+
var aiBaseUrl = document.getElementById('ai-base-url');
|
|
5230
|
+
var aiModel = document.getElementById('ai-model');
|
|
5231
|
+
var selectedProvider = 'anthropic';
|
|
5232
|
+
|
|
5233
|
+
// Provider buttons
|
|
5234
|
+
document.querySelectorAll('.ai-key-provider-btn').forEach(function(btn) {
|
|
5235
|
+
btn.addEventListener('click', function() {
|
|
5236
|
+
document.querySelectorAll('.ai-key-provider-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
5237
|
+
btn.classList.add('active');
|
|
5238
|
+
selectedProvider = btn.getAttribute('data-provider');
|
|
5239
|
+
// Show extra fields for custom/openai providers
|
|
5240
|
+
if (aiKeyExtra) {
|
|
5241
|
+
aiKeyExtra.style.display = (selectedProvider === 'custom' || selectedProvider === 'openai') ? '' : 'none';
|
|
5242
|
+
}
|
|
5243
|
+
// Update placeholder
|
|
5244
|
+
if (aiKeyInput) {
|
|
5245
|
+
if (selectedProvider === 'anthropic') aiKeyInput.placeholder = 'sk-ant-... (Anthropic API key)';
|
|
5246
|
+
else if (selectedProvider === 'openai') aiKeyInput.placeholder = 'sk-... (OpenAI API key)';
|
|
5247
|
+
else aiKeyInput.placeholder = 'API key (or "none" for local models)';
|
|
5248
|
+
}
|
|
5249
|
+
});
|
|
5250
|
+
});
|
|
5251
|
+
|
|
5252
|
+
// Load current config on boot
|
|
5253
|
+
fetch('/api/ai-config').then(function(r) { return r.json(); }).then(function(data) {
|
|
5254
|
+
if (data.hasKey && aiKeyStatus) {
|
|
5255
|
+
aiKeyStatus.textContent = data.keyPreview;
|
|
5256
|
+
aiKeyStatus.className = 'ai-key-status connected';
|
|
5257
|
+
}
|
|
5258
|
+
if (data.provider) {
|
|
5259
|
+
selectedProvider = data.provider;
|
|
5260
|
+
document.querySelectorAll('.ai-key-provider-btn').forEach(function(b) {
|
|
5261
|
+
b.classList.toggle('active', b.getAttribute('data-provider') === data.provider);
|
|
5262
|
+
});
|
|
5263
|
+
if (aiKeyExtra) {
|
|
5264
|
+
aiKeyExtra.style.display = (data.provider === 'custom' || data.provider === 'openai') ? '' : 'none';
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
if (data.baseUrl && aiBaseUrl) aiBaseUrl.value = data.baseUrl;
|
|
5268
|
+
if (data.model && aiModel) aiModel.value = data.model;
|
|
5269
|
+
}).catch(function() {});
|
|
5270
|
+
|
|
5271
|
+
// Save key
|
|
5272
|
+
if (aiKeySaveBtn) aiKeySaveBtn.addEventListener('click', function() {
|
|
5273
|
+
var key = aiKeyInput ? aiKeyInput.value.trim() : '';
|
|
5274
|
+
if (!key) return;
|
|
5275
|
+
var payload = { provider: selectedProvider, apiKey: key };
|
|
5276
|
+
if (aiBaseUrl && aiBaseUrl.value.trim()) payload.baseUrl = aiBaseUrl.value.trim();
|
|
5277
|
+
if (aiModel && aiModel.value.trim()) payload.model = aiModel.value.trim();
|
|
5278
|
+
|
|
5279
|
+
fetch('/api/ai-config', {
|
|
5280
|
+
method: 'POST',
|
|
5281
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5282
|
+
body: JSON.stringify(payload),
|
|
5283
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
5284
|
+
if (data.status === 'updated' && aiKeyStatus) {
|
|
5285
|
+
aiKeyStatus.textContent = 'Connected';
|
|
5286
|
+
aiKeyStatus.className = 'ai-key-status connected';
|
|
5287
|
+
if (aiKeyInput) aiKeyInput.value = '';
|
|
5288
|
+
aiKeyInput.placeholder = 'Key saved — enter new key to change';
|
|
5289
|
+
}
|
|
5290
|
+
}).catch(function(err) {
|
|
5291
|
+
if (aiKeyStatus) { aiKeyStatus.textContent = 'Error'; aiKeyStatus.className = 'ai-key-status missing'; }
|
|
5292
|
+
});
|
|
5293
|
+
});
|
|
5294
|
+
|
|
5295
|
+
// ============================================
|
|
5296
|
+
// INCENTIVE TRACKER (Reward/Penalize)
|
|
5297
|
+
// ============================================
|
|
5298
|
+
var incentiveLog = []; // { agent, type, magnitude, cooldown, round, action }
|
|
5299
|
+
|
|
5300
|
+
function trackIncentive(reaction, round) {
|
|
5301
|
+
if (!reaction || !reaction.verdict) return;
|
|
5302
|
+
var v = reaction.verdict;
|
|
5303
|
+
// Check for incentive data (from governance engine consequence/reward)
|
|
5304
|
+
if (!v.incentive && v.status !== 'REWARD' && v.status !== 'PENALIZE') return;
|
|
5305
|
+
|
|
5306
|
+
var inc = v.incentive || {};
|
|
5307
|
+
var agentState = v.agentState || {};
|
|
5308
|
+
|
|
5309
|
+
incentiveLog.push({
|
|
5310
|
+
agent: reaction.stakeholder_id,
|
|
5311
|
+
type: inc.type || (v.status === 'REWARD' ? 'reward' : 'penalize'),
|
|
5312
|
+
magnitude: inc.magnitude || 1,
|
|
5313
|
+
cooldown: inc.cooldownRounds || agentState.cooldown || 0,
|
|
5314
|
+
round: round,
|
|
5315
|
+
action: reaction.reaction,
|
|
5316
|
+
description: inc.description || v.reason || '',
|
|
5317
|
+
influence: agentState.influence || 1.0,
|
|
5318
|
+
totalPenalties: agentState.penalties || 0,
|
|
5319
|
+
totalRewards: agentState.rewards || 0,
|
|
5320
|
+
});
|
|
5321
|
+
renderIncentiveTracker();
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5324
|
+
function renderIncentiveTracker() {
|
|
5325
|
+
var section = document.getElementById('deck-incentive-section');
|
|
5326
|
+
var summaryEl = document.getElementById('incentive-summary');
|
|
5327
|
+
var agentsEl = document.getElementById('incentive-agents');
|
|
5328
|
+
if (!section || !summaryEl || !agentsEl) return;
|
|
5329
|
+
if (incentiveLog.length === 0) { section.style.display = 'none'; return; }
|
|
5330
|
+
|
|
5331
|
+
section.style.display = '';
|
|
5332
|
+
|
|
5333
|
+
var rewards = incentiveLog.filter(function(i) { return i.type === 'reward'; });
|
|
5334
|
+
var penalties = incentiveLog.filter(function(i) { return i.type === 'penalize'; });
|
|
5335
|
+
|
|
5336
|
+
// Count unique frozen agents (latest state)
|
|
5337
|
+
var latestStates = {};
|
|
5338
|
+
incentiveLog.forEach(function(i) { latestStates[i.agent] = i; });
|
|
5339
|
+
var frozenCount = Object.values(latestStates).filter(function(s) { return s.cooldown > 0; }).length;
|
|
5340
|
+
|
|
5341
|
+
summaryEl.innerHTML =
|
|
5342
|
+
'<div style="display:flex;gap:8px;margin-bottom:12px">' +
|
|
5343
|
+
'<div style="flex:1;text-align:center;padding:8px;background:rgba(74,222,128,0.1);border-radius:6px">' +
|
|
5344
|
+
'<div style="font-size:24px;font-weight:800;color:#4ade80">' + rewards.length + '</div>' +
|
|
5345
|
+
'<div style="font-size:10px;color:#86efac">Rewards</div></div>' +
|
|
5346
|
+
'<div style="flex:1;text-align:center;padding:8px;background:rgba(251,146,60,0.1);border-radius:6px">' +
|
|
5347
|
+
'<div style="font-size:24px;font-weight:800;color:#fb923c">' + penalties.length + '</div>' +
|
|
5348
|
+
'<div style="font-size:10px;color:#fdba74">Penalties</div></div>' +
|
|
5349
|
+
'<div style="flex:1;text-align:center;padding:8px;background:rgba(99,102,241,0.1);border-radius:6px">' +
|
|
5350
|
+
'<div style="font-size:24px;font-weight:800;color:#818cf8">' + frozenCount + '</div>' +
|
|
5351
|
+
'<div style="font-size:10px;color:#a5b4fc">Frozen</div></div>' +
|
|
5352
|
+
'</div>';
|
|
5353
|
+
|
|
5354
|
+
// Show per-agent incentive history (latest 10)
|
|
5355
|
+
var recent = incentiveLog.slice(-10).reverse();
|
|
5356
|
+
var agentHtml = recent.map(function(inc) {
|
|
5357
|
+
var icon = inc.type === 'reward' ? '★' : '⚠';
|
|
5358
|
+
var cls = inc.type === 'reward' ? 'reward' : 'penalize';
|
|
5359
|
+
var cooldownStr = inc.cooldown > 0 ? ' <span style="color:#818cf8;font-size:9px">(frozen ' + inc.cooldown + 'r)</span>' : '';
|
|
5360
|
+
var influenceStr = inc.influence !== undefined && inc.influence !== 1.0 ? ' <span style="color:#a78bfa;font-size:9px">inf:' + inc.influence.toFixed(1) + '</span>' : '';
|
|
5361
|
+
var descStr = inc.description ? '<div style="font-size:9px;color:var(--text-faint);margin-top:2px;padding-left:4px">' + inc.description + '</div>' : '';
|
|
5362
|
+
return '<div class="stream-item" style="flex-wrap:wrap">' +
|
|
5363
|
+
'<span class="incentive-badge ' + cls + '">' + icon + ' ' + inc.type + '</span>' +
|
|
5364
|
+
'<span class="stream-agent">' + inc.agent + '</span>' +
|
|
5365
|
+
'<span class="stream-action">' + normalizeAction(inc.action) + cooldownStr + influenceStr + '</span>' +
|
|
5366
|
+
'<span class="stream-round">R' + inc.round + '</span>' +
|
|
5367
|
+
descStr +
|
|
5368
|
+
'</div>';
|
|
5369
|
+
}).join('');
|
|
5370
|
+
|
|
5371
|
+
agentsEl.innerHTML = agentHtml;
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5374
|
+
// ============================================
|
|
5375
|
+
// ONBOARDING
|
|
5376
|
+
// ============================================
|
|
5377
|
+
var onboardOverlay = document.getElementById('onboard-overlay');
|
|
5378
|
+
var onboardStartBtn = document.getElementById('onboard-start-btn');
|
|
5379
|
+
var onboardDismiss = document.getElementById('onboard-dismiss');
|
|
5380
|
+
var startHereBtn = document.getElementById('start-here-btn');
|
|
5381
|
+
|
|
5382
|
+
function showOnboarding() {
|
|
5383
|
+
if (onboardOverlay) onboardOverlay.style.display = '';
|
|
5384
|
+
}
|
|
5385
|
+
|
|
5386
|
+
function hideOnboarding(permanent) {
|
|
5387
|
+
if (onboardOverlay) onboardOverlay.style.display = 'none';
|
|
5388
|
+
if (permanent) localStorage.setItem('nv-onboarded', '1');
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5391
|
+
// "Start" = close and mark as seen
|
|
5392
|
+
if (onboardStartBtn) onboardStartBtn.addEventListener('click', function() { hideOnboarding(true); });
|
|
5393
|
+
// "Don't show again" = same
|
|
5394
|
+
if (onboardDismiss) onboardDismiss.addEventListener('click', function() { hideOnboarding(true); });
|
|
5395
|
+
// Close on backdrop click
|
|
5396
|
+
if (onboardOverlay) onboardOverlay.addEventListener('click', function(e) { if (e.target === onboardOverlay) hideOnboarding(false); });
|
|
5397
|
+
// "Start Here" button in left panel = always show
|
|
5398
|
+
if (startHereBtn) startHereBtn.addEventListener('click', showOnboarding);
|
|
5399
|
+
|
|
5400
|
+
// Show on first visit
|
|
5401
|
+
if (!localStorage.getItem('nv-onboarded')) {
|
|
5402
|
+
showOnboarding();
|
|
5403
|
+
}
|
|
5404
|
+
|
|
3615
5405
|
// ============================================
|
|
3616
5406
|
// BOOT
|
|
3617
5407
|
// ============================================
|