@neuroverseos/nv-sim 0.1.9 → 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 +90 -3
- 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-D2ZrMny8.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/cli.js +89 -114
- 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 -28
- package/dist/engine/intentTranslator.js +281 -0
- package/dist/engine/liveAdapter.js +100 -18
- package/dist/engine/liveVisualizer.js +2071 -1023
- package/dist/engine/primeRadiant.js +2 -8
- package/dist/engine/reasoningEngine.js +2 -7
- package/dist/engine/scenarioCapsule.js +5 -5
- package/dist/engine/swarmSimulation.js +1 -9
- package/dist/engine/worldBridge.js +22 -8
- 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-B64NuIXu.css +0 -1
- package/dist/assets/index-BMkPevVr.js +0 -532
- 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");
|
|
@@ -186,24 +187,90 @@ function startInteractiveServer(port, onReady) {
|
|
|
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;
|
|
189
202
|
// ── Bridge Metrics Tracker ──
|
|
190
203
|
// Computes meaningful live metrics from external /api/evaluate calls
|
|
191
204
|
let bridgeEvalCount = 0;
|
|
192
205
|
let bridgeBlockCount = 0;
|
|
193
206
|
let bridgeModifyCount = 0;
|
|
207
|
+
let bridgeRewardCount = 0;
|
|
208
|
+
let bridgePenalizeCount = 0;
|
|
194
209
|
function computeBridgeMetrics(decision) {
|
|
195
210
|
bridgeEvalCount++;
|
|
196
211
|
if (decision === "BLOCK")
|
|
197
212
|
bridgeBlockCount++;
|
|
198
213
|
if (decision === "MODIFY")
|
|
199
214
|
bridgeModifyCount++;
|
|
200
|
-
|
|
215
|
+
if (decision === "REWARD")
|
|
216
|
+
bridgeRewardCount++;
|
|
217
|
+
if (decision === "PENALIZE")
|
|
218
|
+
bridgePenalizeCount++;
|
|
219
|
+
const totalInterventions = bridgeBlockCount + bridgeModifyCount + bridgePenalizeCount;
|
|
201
220
|
// Stability = ratio of non-blocked actions (higher = more stable)
|
|
202
|
-
const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount) / bridgeEvalCount : 1;
|
|
203
|
-
// Volatility = ratio of interventions (blocks + modifies) — more interventions = more volatile
|
|
221
|
+
const stability = bridgeEvalCount > 0 ? (bridgeEvalCount - bridgeBlockCount - bridgePenalizeCount) / bridgeEvalCount : 1;
|
|
222
|
+
// Volatility = ratio of interventions (blocks + modifies + penalizes) — more interventions = more volatile
|
|
204
223
|
const volatility = bridgeEvalCount > 0 ? totalInterventions / bridgeEvalCount : 0;
|
|
205
224
|
return { stability, volatility, totalInterventions, evalCount: bridgeEvalCount };
|
|
206
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
|
+
}
|
|
207
274
|
function synthesizeSessionReport() {
|
|
208
275
|
// Gather all sessions to report on (history + current if it has data)
|
|
209
276
|
const allSessions = [
|
|
@@ -554,24 +621,47 @@ function startInteractiveServer(port, onReady) {
|
|
|
554
621
|
try {
|
|
555
622
|
const body = await readBody(req);
|
|
556
623
|
const payload = JSON.parse(body);
|
|
557
|
-
if (!payload.name
|
|
558
|
-
jsonResponse(res, 400, { error: "name
|
|
624
|
+
if (!payload.name) {
|
|
625
|
+
jsonResponse(res, 400, { error: "name is required" });
|
|
559
626
|
return;
|
|
560
627
|
}
|
|
561
628
|
const id = slugify(payload.name);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
+
}
|
|
575
665
|
}
|
|
576
666
|
catch (err) {
|
|
577
667
|
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
@@ -689,7 +779,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
689
779
|
default_enabled: true,
|
|
690
780
|
},
|
|
691
781
|
];
|
|
692
|
-
// Social media guards for
|
|
782
|
+
// Social media guards for OASIS agent actions.
|
|
693
783
|
// These govern what AI agents can do on simulated social platforms.
|
|
694
784
|
const socialGuards = [
|
|
695
785
|
{
|
|
@@ -759,6 +849,8 @@ function startInteractiveServer(port, onReady) {
|
|
|
759
849
|
immutable: false,
|
|
760
850
|
intent_patterns: cg.intent_patterns,
|
|
761
851
|
default_enabled: true,
|
|
852
|
+
...(cg.consequence ? { consequence: cg.consequence } : {}),
|
|
853
|
+
...(cg.reward ? { reward: cg.reward } : {}),
|
|
762
854
|
});
|
|
763
855
|
// Add patterns to vocabulary
|
|
764
856
|
for (const pat of cg.intent_patterns) {
|
|
@@ -816,6 +908,90 @@ function startInteractiveServer(port, onReady) {
|
|
|
816
908
|
});
|
|
817
909
|
return;
|
|
818
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
|
+
}
|
|
819
995
|
// Build a proper GuardEvent — the guard engine matches intent against intent_patterns.
|
|
820
996
|
// Omit `direction` — setting it enables execution-intent safety checks (prompt injection
|
|
821
997
|
// detection) which falsely flag financial terms like "buy" and "sell". Bridge actions are
|
|
@@ -830,6 +1006,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
830
1006
|
actor: payload.actor,
|
|
831
1007
|
action: payload.action,
|
|
832
1008
|
...(payload.payload ?? {}),
|
|
1009
|
+
...(payload.state ? { state: payload.state } : {}),
|
|
833
1010
|
},
|
|
834
1011
|
};
|
|
835
1012
|
// Evaluate directly via the governance module
|
|
@@ -848,12 +1025,77 @@ function startInteractiveServer(port, onReady) {
|
|
|
848
1025
|
stakeholders: [{ id: payload.actor, description: payload.actor, disposition: "neutral", priorities: [] }],
|
|
849
1026
|
}, nvWorld, { trace: true, level: "standard" });
|
|
850
1027
|
}
|
|
851
|
-
// Map verdict to bridge protocol
|
|
1028
|
+
// Map verdict to bridge protocol — includes REWARD/PENALIZE from governance engine
|
|
852
1029
|
const decision = verdict.status === "BLOCK" ? "BLOCK"
|
|
853
1030
|
: verdict.status === "PAUSE" ? "MODIFY"
|
|
854
|
-
: "
|
|
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;
|
|
855
1090
|
// Compute live metrics from cumulative bridge evaluations
|
|
856
|
-
const bridgeMetrics = computeBridgeMetrics(
|
|
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;
|
|
857
1099
|
// Broadcast governance event to connected SSE clients
|
|
858
1100
|
broadcast({
|
|
859
1101
|
type: "round",
|
|
@@ -863,15 +1105,19 @@ function startInteractiveServer(port, onReady) {
|
|
|
863
1105
|
reactions: [{
|
|
864
1106
|
stakeholder_id: payload.actor,
|
|
865
1107
|
reaction: payload.action,
|
|
866
|
-
impact:
|
|
867
|
-
confidence: 0.5,
|
|
1108
|
+
impact: impactForDecision,
|
|
1109
|
+
confidence: 0.5 * agentState.rewardMultiplier,
|
|
868
1110
|
trigger: "bridge",
|
|
869
|
-
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
|
+
},
|
|
870
1116
|
}],
|
|
871
|
-
avgImpact:
|
|
1117
|
+
avgImpact: impactForDecision,
|
|
872
1118
|
maxVolatility: bridgeMetrics.volatility,
|
|
873
1119
|
dynamics: [],
|
|
874
|
-
interventionCount:
|
|
1120
|
+
interventionCount: effectiveDecision !== "ALLOW" && effectiveDecision !== "REWARD" ? 1 : 0,
|
|
875
1121
|
});
|
|
876
1122
|
// Also broadcast a stability update so the metric refreshes live
|
|
877
1123
|
broadcast({
|
|
@@ -884,10 +1130,11 @@ function startInteractiveServer(port, onReady) {
|
|
|
884
1130
|
// Record in session
|
|
885
1131
|
currentSession.evaluations.push({
|
|
886
1132
|
actor: payload.actor, action: payload.action,
|
|
887
|
-
decision:
|
|
1133
|
+
decision: effectiveDecision,
|
|
888
1134
|
reason: verdict.reason ?? "", ruleId: verdict.ruleId ?? null,
|
|
889
1135
|
world: payload.world ?? currentSession.world,
|
|
890
1136
|
timestamp: Date.now(), payload: payload.payload,
|
|
1137
|
+
incentive,
|
|
891
1138
|
});
|
|
892
1139
|
currentSession.guardCount = customGuards.length + (nvWorld.guards?.guards?.length ?? 0);
|
|
893
1140
|
// Persist to audit trail on disk
|
|
@@ -895,23 +1142,32 @@ function startInteractiveServer(port, onReady) {
|
|
|
895
1142
|
agent: payload.actor,
|
|
896
1143
|
action: payload.action,
|
|
897
1144
|
actionType: payload.payload?.type ?? "unknown",
|
|
898
|
-
verdict:
|
|
1145
|
+
verdict: effectiveDecision,
|
|
899
1146
|
reason: verdict.reason ?? "",
|
|
900
1147
|
confidence: verdict.confidence ?? 0.5,
|
|
901
1148
|
rulesFired: verdict.ruleId ? [{
|
|
902
1149
|
id: verdict.ruleId,
|
|
903
1150
|
description: verdict.reason ?? "",
|
|
904
|
-
effect:
|
|
905
|
-
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,
|
|
906
1153
|
}] : [],
|
|
907
1154
|
worldState: payload.world ?? currentSession.world,
|
|
908
1155
|
});
|
|
909
1156
|
jsonResponse(res, 200, {
|
|
910
|
-
decision,
|
|
1157
|
+
decision: effectiveDecision,
|
|
911
1158
|
reason: verdict.reason ?? null,
|
|
912
1159
|
rule_id: verdict.ruleId ?? null,
|
|
913
1160
|
evidence: verdict.evidence ?? null,
|
|
914
|
-
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
|
+
},
|
|
915
1171
|
});
|
|
916
1172
|
}
|
|
917
1173
|
catch (err) {
|
|
@@ -1025,6 +1281,8 @@ function startInteractiveServer(port, onReady) {
|
|
|
1025
1281
|
immutable: false,
|
|
1026
1282
|
intent_patterns: rule.intent_patterns,
|
|
1027
1283
|
default_enabled: true,
|
|
1284
|
+
...(rule.consequence ? { consequence: rule.consequence } : {}),
|
|
1285
|
+
...(rule.reward ? { reward: rule.reward } : {}),
|
|
1028
1286
|
});
|
|
1029
1287
|
}
|
|
1030
1288
|
jsonResponse(res, 200, {
|
|
@@ -1169,6 +1427,8 @@ function startInteractiveServer(port, onReady) {
|
|
|
1169
1427
|
const blocked = evals.filter(e => e.decision === "BLOCK").length;
|
|
1170
1428
|
const modified = evals.filter(e => e.decision === "MODIFY").length;
|
|
1171
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;
|
|
1172
1432
|
const uniqueActors = [...new Set(evals.map(e => e.actor))];
|
|
1173
1433
|
const uniqueActions = [...new Set(evals.map(e => e.action))];
|
|
1174
1434
|
const triggeredRules = [...new Set(evals.filter(e => e.ruleId).map(e => e.ruleId))];
|
|
@@ -1182,6 +1442,8 @@ function startInteractiveServer(port, onReady) {
|
|
|
1182
1442
|
blocked,
|
|
1183
1443
|
modified,
|
|
1184
1444
|
allowed,
|
|
1445
|
+
rewarded,
|
|
1446
|
+
penalized,
|
|
1185
1447
|
},
|
|
1186
1448
|
agents: uniqueActors,
|
|
1187
1449
|
actionTypes: uniqueActions,
|
|
@@ -1388,6 +1650,308 @@ function startInteractiveServer(port, onReady) {
|
|
|
1388
1650
|
jsonResponse(res, 200, { adapters });
|
|
1389
1651
|
return;
|
|
1390
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
|
+
}
|
|
1391
1955
|
// Run simulation via live adapter (external process)
|
|
1392
1956
|
if (req.url === "/api/run-live" && req.method === "POST") {
|
|
1393
1957
|
if (isRunning) {
|
|
@@ -1441,7 +2005,7 @@ function startInteractiveServer(port, onReady) {
|
|
|
1441
2005
|
// Listen for rounds from the adapter
|
|
1442
2006
|
adapter.on("round", (liveRound) => {
|
|
1443
2007
|
// Build lookup of original verdicts from adapter's adaptation data.
|
|
1444
|
-
// When an external bridge
|
|
2008
|
+
// When an external bridge already applied governance,
|
|
1445
2009
|
// the verdict lives in adaptation.deltas — use it instead of re-evaluating
|
|
1446
2010
|
// (re-evaluating the MODIFIED action would return ALLOW, hiding the BLOCK).
|
|
1447
2011
|
const adaptationByAgent = new Map();
|
|
@@ -1958,6 +2522,185 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
1958
2522
|
.scenario-btn { padding: 6px; }
|
|
1959
2523
|
}
|
|
1960
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
|
+
|
|
1961
2704
|
/* Rule editor */
|
|
1962
2705
|
.rule-editor { margin-top: 8px; }
|
|
1963
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; }
|
|
@@ -2047,7 +2790,7 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
2047
2790
|
<body>
|
|
2048
2791
|
<div class="header">
|
|
2049
2792
|
<div style="display:flex;align-items:center">
|
|
2050
|
-
<h1>
|
|
2793
|
+
<h1>NeuroVerse</h1>
|
|
2051
2794
|
<span class="sub">Governance Runtime</span>
|
|
2052
2795
|
</div>
|
|
2053
2796
|
<div class="header-right">
|
|
@@ -2059,116 +2802,58 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
2059
2802
|
<div class="layout">
|
|
2060
2803
|
<!-- LEFT: CONTROLS -->
|
|
2061
2804
|
<div class="controls" id="controls-panel">
|
|
2062
|
-
<!-- World Action Bar -->
|
|
2063
|
-
<div class="world-action-bar">
|
|
2064
|
-
<button class="btn btn-world-action" id="new-world-btn" title="Clear everything and start fresh">+ New World</button>
|
|
2065
|
-
<button class="btn btn-world-action" id="load-file-btn" title="Load a .json world file">Load World File</button>
|
|
2066
|
-
<button class="btn btn-world-action" id="clear-rules-btn" title="Clear custom rules only">Clear Rules</button>
|
|
2067
|
-
<button class="btn btn-world-action btn-export" id="export-world-btn" title="Export current world as JSON">Save as World File</button>
|
|
2068
|
-
</div>
|
|
2069
2805
|
|
|
2070
|
-
<!--
|
|
2071
|
-
<
|
|
2072
|
-
<h3>World Source</h3>
|
|
2073
|
-
<div class="world-source-tabs">
|
|
2074
|
-
<label class="ws-tab active" data-source="preset">
|
|
2075
|
-
<input type="radio" name="world-source" value="preset" checked>
|
|
2076
|
-
<span class="ws-label">Preset</span>
|
|
2077
|
-
<span class="ws-hint">Demo scenarios</span>
|
|
2078
|
-
</label>
|
|
2079
|
-
<label class="ws-tab" data-source="custom">
|
|
2080
|
-
<input type="radio" name="world-source" value="custom">
|
|
2081
|
-
<span class="ws-label">Custom Rules</span>
|
|
2082
|
-
<span class="ws-hint">Define your world</span>
|
|
2083
|
-
</label>
|
|
2084
|
-
<label class="ws-tab" data-source="upload">
|
|
2085
|
-
<input type="radio" name="world-source" value="upload">
|
|
2086
|
-
<span class="ws-label">World File</span>
|
|
2087
|
-
<span class="ws-hint">JSON / .nv-world</span>
|
|
2088
|
-
</label>
|
|
2089
|
-
</div>
|
|
2090
|
-
</div>
|
|
2806
|
+
<!-- Start Here -->
|
|
2807
|
+
<button class="btn-start-here" id="start-here-btn">Start Here</button>
|
|
2091
2808
|
|
|
2092
|
-
<!--
|
|
2093
|
-
<div class="
|
|
2094
|
-
<div class="
|
|
2095
|
-
<
|
|
2096
|
-
<
|
|
2097
|
-
<select id="world-select"></select>
|
|
2098
|
-
</div>
|
|
2099
|
-
<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>
|
|
2100
2814
|
</div>
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
<
|
|
2105
|
-
<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>
|
|
2106
2819
|
</div>
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
<
|
|
2111
|
-
<div id="scenario-list" style="display:none"></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)">
|
|
2112
2824
|
</div>
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
<div class="world-source-panel" id="source-custom" style="display:none">
|
|
2117
|
-
<div class="ctrl-section">
|
|
2118
|
-
<h3>Define Your World</h3>
|
|
2119
|
-
<div class="custom-world-header">
|
|
2120
|
-
<input type="text" class="world-name-input" id="custom-world-name" placeholder="World name (e.g. Marketing Governance)">
|
|
2121
|
-
<textarea class="world-thesis-input" id="custom-world-thesis" placeholder="What is this world about? (thesis)"></textarea>
|
|
2122
|
-
</div>
|
|
2123
|
-
<div class="rule-editor">
|
|
2124
|
-
<div class="rule-editor-label">Type your governance rules:</div>
|
|
2125
|
-
<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>
|
|
2126
|
-
<button class="btn btn-generate-world" id="parse-rules-btn">Generate World</button>
|
|
2127
|
-
<div id="parsed-rules" class="parsed-rules"></div>
|
|
2128
|
-
<div id="rule-status" class="rule-status"></div>
|
|
2129
|
-
<div class="rule-examples">
|
|
2130
|
-
Rule patterns:<br>
|
|
2131
|
-
<code>Block [action]</code> — hard suppression<br>
|
|
2132
|
-
<code>Limit [X] to [N]</code> — cap extremes<br>
|
|
2133
|
-
<code>Require [X] for [Y]</code> — structural constraint<br>
|
|
2134
|
-
<code>Pause [X] for review</code> — human-in-the-loop<br>
|
|
2135
|
-
<code>Allow [X]</code> — explicit permission<br>
|
|
2136
|
-
<code>Monitor [X]</code> — circuit breaker gate
|
|
2137
|
-
</div>
|
|
2138
|
-
</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>
|
|
2139
2828
|
</div>
|
|
2829
|
+
</div>
|
|
2140
2830
|
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
<div style="font-size:10px;color:var(--text-faint);margin-top:4px">Layer your rules on top of a preset world</div>
|
|
2149
|
-
</div>
|
|
2150
|
-
</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>
|
|
2151
2838
|
</div>
|
|
2152
2839
|
|
|
2153
|
-
<!--
|
|
2154
|
-
<div class="
|
|
2155
|
-
<div class="
|
|
2156
|
-
<
|
|
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">
|
|
2157
2846
|
<div class="upload-zone" id="upload-zone">
|
|
2158
|
-
<div class="upload-icon">📄</div>
|
|
2159
2847
|
<div class="upload-label">Drop a .json or .nv-world file here</div>
|
|
2160
2848
|
<div class="upload-or">or</div>
|
|
2161
2849
|
<button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
|
|
2162
2850
|
<input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
|
|
2163
2851
|
</div>
|
|
2164
|
-
<div class="upload-paste-section">
|
|
2165
|
-
<
|
|
2166
|
-
<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>
|
|
2167
2854
|
</div>
|
|
2168
|
-
<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>
|
|
2169
2856
|
<div id="upload-status" class="rule-status"></div>
|
|
2170
|
-
|
|
2171
|
-
<!-- Loaded world info -->
|
|
2172
2857
|
<div id="loaded-world-info" style="display:none">
|
|
2173
2858
|
<div class="loaded-world-card">
|
|
2174
2859
|
<div class="lw-name" id="lw-name"></div>
|
|
@@ -2176,65 +2861,54 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
2176
2861
|
<div class="lw-stats" id="lw-stats"></div>
|
|
2177
2862
|
</div>
|
|
2178
2863
|
</div>
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
<!-- World file schema reference -->
|
|
2182
|
-
<div class="ctrl-section">
|
|
2183
|
-
<h3>World File Schema</h3>
|
|
2184
|
-
<div class="schema-ref">
|
|
2185
|
-
<div class="schema-item"><code>name</code> — world name</div>
|
|
2186
|
-
<div class="schema-item"><code>thesis</code> — what this world is about</div>
|
|
2187
|
-
<div class="schema-item"><code>rules[]</code> — governance rules (plain English or structured)</div>
|
|
2188
|
-
<div class="schema-item"><code>invariants[]</code> — rules that always hold <code>{id, description}</code></div>
|
|
2189
|
-
<div class="schema-item"><code>gates[]</code> — viability thresholds <code>{id, label, condition, severity}</code></div>
|
|
2190
|
-
<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>
|
|
2191
2866
|
</div>
|
|
2192
2867
|
</div>
|
|
2193
2868
|
</div>
|
|
2194
2869
|
|
|
2195
|
-
<!--
|
|
2196
|
-
<div class="ctrl-section"
|
|
2197
|
-
<h3>
|
|
2198
|
-
<div class="
|
|
2199
|
-
<
|
|
2200
|
-
<
|
|
2201
|
-
|
|
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>
|
|
2202
2886
|
</div>
|
|
2203
|
-
<div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
|
|
2204
2887
|
</div>
|
|
2205
2888
|
|
|
2206
|
-
<!--
|
|
2207
|
-
<div class="ctrl-section">
|
|
2208
|
-
<h3>
|
|
2209
|
-
<div
|
|
2210
|
-
<select id="event-select"></select>
|
|
2211
|
-
<input type="number" id="event-round" min="1" max="20" value="3" placeholder="R">
|
|
2212
|
-
</div>
|
|
2213
|
-
<button class="btn btn-add" id="add-event-btn">+ Add Event</button>
|
|
2214
|
-
<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>
|
|
2215
2893
|
</div>
|
|
2216
2894
|
|
|
2217
|
-
<!--
|
|
2218
|
-
<div class="ctrl-section">
|
|
2219
|
-
<h3>
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
<span class="val" id="rounds-val">5</span>
|
|
2224
|
-
</div>
|
|
2225
|
-
<input type="range" id="rounds-slider" min="3" max="12" value="5">
|
|
2226
|
-
</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>
|
|
2227
2901
|
</div>
|
|
2228
2902
|
|
|
2229
2903
|
<!-- Run button -->
|
|
2230
|
-
<button class="btn btn-run" id="run-btn">Run
|
|
2904
|
+
<button class="btn btn-run" id="run-btn">Run Governance</button>
|
|
2231
2905
|
|
|
2232
|
-
<!-- Save as
|
|
2906
|
+
<!-- Save as Simulation Rules -->
|
|
2233
2907
|
<div id="save-section" style="margin-top:12px">
|
|
2234
|
-
<button class="btn btn-save" id="save-btn">Save as
|
|
2908
|
+
<button class="btn btn-save" id="save-btn">Save as Simulation Rules</button>
|
|
2235
2909
|
<div id="save-form" style="display:none;margin-top:8px">
|
|
2236
|
-
<input type="text" id="variant-name" placeholder="
|
|
2237
|
-
<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">
|
|
2238
2912
|
<div style="display:flex;gap:6px;margin-top:6px">
|
|
2239
2913
|
<button class="btn btn-confirm" id="confirm-save-btn">Save</button>
|
|
2240
2914
|
<button class="btn btn-cancel" id="cancel-save-btn">Cancel</button>
|
|
@@ -2242,218 +2916,28 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
2242
2916
|
</div>
|
|
2243
2917
|
</div>
|
|
2244
2918
|
|
|
2245
|
-
<!-- Saved
|
|
2919
|
+
<!-- Saved Simulation Rules -->
|
|
2246
2920
|
<div class="ctrl-section" style="margin-top:16px">
|
|
2247
|
-
<h3>
|
|
2248
|
-
<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>
|
|
2249
2923
|
</div>
|
|
2250
2924
|
|
|
2251
|
-
<!--
|
|
2252
|
-
<div class="ctrl-section" style="margin-top:16px">
|
|
2253
|
-
<h3>
|
|
2254
|
-
|
|
2255
|
-
<div class="arch-callout">
|
|
2256
|
-
<div class="arch-title">Pre-Execution Enforcement</div>
|
|
2257
|
-
<div class="arch-flow">
|
|
2258
|
-
Agent decides → <span class="arch-yes">Governance evaluates</span> → <span class="arch-yes">THEN executes</span>
|
|
2259
|
-
</div>
|
|
2260
|
-
<div class="arch-warn">Governance MUST happen BEFORE execution. Post-execution = audit only, not control.</div>
|
|
2261
|
-
</div>
|
|
2262
|
-
|
|
2263
|
-
<div class="fw-tabs">
|
|
2264
|
-
<button class="fw-tab active" onclick="switchFw('generic',this)">Generic<span class="fw-sub">any agent</span></button>
|
|
2265
|
-
<button class="fw-tab" onclick="switchFw('scienceclaw',this)">ScienceClaw<span class="fw-sub">research</span></button>
|
|
2266
|
-
<button class="fw-tab" onclick="switchFw('mirofish',this)">MiroFish<span class="fw-sub">OASIS</span></button>
|
|
2267
|
-
<button class="fw-tab" onclick="switchFw('langchain',this)">LangChain<span class="fw-sub">tools</span></button>
|
|
2268
|
-
</div>
|
|
2269
|
-
|
|
2270
|
-
<div class="fw-panel active" id="fw-generic">
|
|
2271
|
-
<div class="integrate-section">
|
|
2272
|
-
<h4>Insert before your agent executes</h4>
|
|
2273
|
-
<div class="lang-tabs">
|
|
2274
|
-
<button class="lang-tab active" onclick="switchLang('generic-py',this)">Python</button>
|
|
2275
|
-
<button class="lang-tab" onclick="switchLang('generic-js',this)">JavaScript</button>
|
|
2276
|
-
<button class="lang-tab" onclick="switchLang('generic-curl',this)">cURL</button>
|
|
2277
|
-
</div>
|
|
2278
|
-
<div class="lang-code-panel active" id="lang-generic-py"><div class="integrate-code"><span class="kw">import</span> requests
|
|
2279
|
-
|
|
2280
|
-
<span class="kw">for</span> agent <span class="kw">in</span> agents:
|
|
2281
|
-
raw_action = agent.decide()
|
|
2282
|
-
|
|
2283
|
-
<span class="kw">try</span>:
|
|
2284
|
-
verdict = requests.post(
|
|
2285
|
-
<span class="str">"http://localhost:3456/api/evaluate"</span>,
|
|
2286
|
-
json={<span class="str">"actor"</span>: agent.id,
|
|
2287
|
-
<span class="str">"action"</span>: raw_action,
|
|
2288
|
-
<span class="str">"world"</span>: <span class="str">"trading"</span>},
|
|
2289
|
-
timeout=0.5
|
|
2290
|
-
).json()
|
|
2291
|
-
<span class="kw">except</span>:
|
|
2292
|
-
verdict = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
|
|
2293
|
-
|
|
2294
|
-
<span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
|
|
2295
|
-
<span class="kw">continue</span> <span class="comment"># skip this action entirely</span>
|
|
2296
|
-
<span class="kw">elif</span> verdict[<span class="str">"decision"</span>] == <span class="str">"MODIFY"</span>:
|
|
2297
|
-
raw_action = verdict[<span class="str">"modified_action"</span>]
|
|
2298
|
-
|
|
2299
|
-
environment.apply(agent, raw_action)</div></div>
|
|
2300
|
-
<div class="lang-code-panel" id="lang-generic-js"><div class="integrate-code"><span class="kw">for</span> (<span class="kw">const</span> agent <span class="kw">of</span> agents) {
|
|
2301
|
-
<span class="kw">let</span> action = agent.decide()
|
|
2302
|
-
|
|
2303
|
-
<span class="kw">const</span> verdict = <span class="kw">await</span> fetch(
|
|
2304
|
-
<span class="str">"http://localhost:3456/api/evaluate"</span>, {
|
|
2305
|
-
method: <span class="str">"POST"</span>,
|
|
2306
|
-
headers: {<span class="str">"Content-Type"</span>: <span class="str">"application/json"</span>},
|
|
2307
|
-
body: JSON.stringify({
|
|
2308
|
-
actor: agent.id, action, world: <span class="str">"trading"</span>
|
|
2309
|
-
})
|
|
2310
|
-
}).then(r => r.json())
|
|
2311
|
-
|
|
2312
|
-
<span class="kw">if</span> (verdict.decision === <span class="str">"BLOCK"</span>) <span class="kw">continue</span>
|
|
2313
|
-
<span class="kw">if</span> (verdict.decision === <span class="str">"MODIFY"</span>)
|
|
2314
|
-
action = verdict.modified_action
|
|
2315
|
-
|
|
2316
|
-
environment.apply(agent, action)
|
|
2317
|
-
}</div></div>
|
|
2318
|
-
<div class="lang-code-panel" id="lang-generic-curl"><div class="integrate-code">curl -X POST http://localhost:3456/api/evaluate \
|
|
2319
|
-
-H <span class="str">"Content-Type: application/json"</span> \
|
|
2320
|
-
-d <span class="str">'{"actor":"agent_1","action":"panic_sell"}'</span>
|
|
2321
|
-
|
|
2322
|
-
<span class="comment"># Returns: {"decision":"BLOCK","reason":"..."}</span>
|
|
2323
|
-
<span class="comment"># Watch it appear in the dashboard →</span></div></div>
|
|
2324
|
-
</div>
|
|
2325
|
-
</div>
|
|
2326
|
-
|
|
2327
|
-
<div class="fw-panel" id="fw-scienceclaw">
|
|
2328
|
-
<div class="integrate-section">
|
|
2329
|
-
<h4>Detect action type, then evaluate</h4>
|
|
2330
|
-
<div style="font-size:10px;color:var(--yellow);margin-bottom:6px;line-height:1.4">
|
|
2331
|
-
Don't hardcode <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">action = "post"</code> — detect intent from agent output so rules can differentiate.
|
|
2332
|
-
</div>
|
|
2333
|
-
<div class="integrate-code"><span class="kw">from</span> neuroverse_bridge <span class="kw">import</span> evaluate, detect_action_type
|
|
2334
|
-
|
|
2335
|
-
<span class="comment"># Step 1: Agent generates content</span>
|
|
2336
|
-
results = generator.run_pubmed_search(topic)
|
|
2337
|
-
output = generator.generate_content(topic, results)
|
|
2338
|
-
|
|
2339
|
-
<span class="comment"># Step 2: Detect what kind of action this is</span>
|
|
2340
|
-
action = detect_action_type(output[<span class="str">"content"</span>])
|
|
2341
|
-
<span class="comment"># → "analyze", "publish", "cite", "recommend"</span>
|
|
2342
|
-
|
|
2343
|
-
<span class="comment"># Step 3: Evaluate BEFORE executing</span>
|
|
2344
|
-
verdict = evaluate(
|
|
2345
|
-
actor=<span class="str">"Harry"</span>,
|
|
2346
|
-
action=action,
|
|
2347
|
-
world=<span class="str">"research"</span>
|
|
2348
|
-
)
|
|
2349
|
-
|
|
2350
|
-
<span class="comment"># Step 4: Only execute if allowed</span>
|
|
2351
|
-
<span class="kw">if</span> verdict[<span class="str">"decision"</span>] != <span class="str">"BLOCK"</span>:
|
|
2352
|
-
generator.post_to_infinite(output)</div>
|
|
2353
|
-
<div style="font-size:9px;color:var(--text-muted);margin-top:6px;line-height:1.5">
|
|
2354
|
-
See <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">bridge/scienceclaw_governed.py</code> for full example
|
|
2355
|
-
</div>
|
|
2356
|
-
</div>
|
|
2357
|
-
</div>
|
|
2358
|
-
|
|
2359
|
-
<div class="fw-panel" id="fw-mirofish">
|
|
2360
|
-
<div class="integrate-section">
|
|
2361
|
-
<h4>Replace the dict comprehension</h4>
|
|
2362
|
-
<div style="font-size:10px;color:var(--red);margin-bottom:6px;line-height:1.4">
|
|
2363
|
-
MiroFish executes inside <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">env.step()</code> — governance MUST happen before that call.
|
|
2364
|
-
</div>
|
|
2365
|
-
<div class="integrate-code" style="margin-bottom:6px;border-left:3px solid var(--red);opacity:0.6"><span class="comment"># ❌ BEFORE (opaque — no governance possible)</span>
|
|
2366
|
-
actions = {agent: LLMAction()
|
|
2367
|
-
<span class="kw">for</span> _, agent <span class="kw">in</span> active_agents}
|
|
2368
|
-
<span class="kw">await</span> result.env.step(actions)</div>
|
|
2369
|
-
<div class="integrate-code" style="border-left:3px solid var(--green)"><span class="comment"># ✅ AFTER (expand the loop, insert governance)</span>
|
|
2370
|
-
<span class="kw">import</span> requests
|
|
2371
|
-
|
|
2372
|
-
actions = {}
|
|
2373
|
-
<span class="kw">for</span> _, agent <span class="kw">in</span> active_agents:
|
|
2374
|
-
raw_action = LLMAction()
|
|
2375
|
-
|
|
2376
|
-
<span class="kw">try</span>:
|
|
2377
|
-
verdict = requests.post(
|
|
2378
|
-
<span class="str">"http://localhost:3456/api/evaluate"</span>,
|
|
2379
|
-
json={
|
|
2380
|
-
<span class="str">"actor"</span>: getattr(agent, <span class="str">"id"</span>, str(agent)),
|
|
2381
|
-
<span class="str">"action"</span>: str(raw_action),
|
|
2382
|
-
<span class="str">"world"</span>: <span class="str">"social_media"</span>
|
|
2383
|
-
},
|
|
2384
|
-
timeout=0.5
|
|
2385
|
-
).json()
|
|
2386
|
-
<span class="kw">except</span>:
|
|
2387
|
-
verdict = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
|
|
2388
|
-
|
|
2389
|
-
<span class="kw">if</span> verdict[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
|
|
2390
|
-
<span class="kw">continue</span>
|
|
2391
|
-
<span class="kw">elif</span> verdict[<span class="str">"decision"</span>] == <span class="str">"MODIFY"</span>:
|
|
2392
|
-
raw_action = verdict.get(<span class="str">"modified_action"</span>, raw_action)
|
|
2393
|
-
|
|
2394
|
-
actions[agent] = raw_action
|
|
2395
|
-
|
|
2396
|
-
<span class="kw">await</span> result.env.step(actions)</div>
|
|
2397
|
-
<div style="font-size:9px;color:var(--text-muted);margin-top:6px;line-height:1.5">
|
|
2398
|
-
Apply to all 3 scripts: <code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_twitter_simulation.py</code>
|
|
2399
|
-
<code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_reddit_simulation.py</code>
|
|
2400
|
-
<code style="background:var(--bg-surface);padding:1px 4px;border-radius:2px">run_parallel_simulation.py</code>
|
|
2401
|
-
</div>
|
|
2402
|
-
</div>
|
|
2403
|
-
</div>
|
|
2404
|
-
|
|
2405
|
-
<div class="fw-panel" id="fw-langchain">
|
|
2406
|
-
<div class="integrate-section">
|
|
2407
|
-
<h4>Wrap your tool or agent executor</h4>
|
|
2408
|
-
<div class="integrate-code"><span class="kw">import</span> requests
|
|
2409
|
-
<span class="kw">from</span> langchain.tools <span class="kw">import</span> tool
|
|
2410
|
-
|
|
2411
|
-
<span class="kw">def</span> governed(fn, world=<span class="str">"trading"</span>):
|
|
2412
|
-
<span class="kw">def</span> wrapper(*args, **kwargs):
|
|
2413
|
-
action = fn.__name__
|
|
2414
|
-
<span class="kw">try</span>:
|
|
2415
|
-
v = requests.post(
|
|
2416
|
-
<span class="str">"http://localhost:3456/api/evaluate"</span>,
|
|
2417
|
-
json={<span class="str">"actor"</span>: <span class="str">"langchain"</span>,
|
|
2418
|
-
<span class="str">"action"</span>: action,
|
|
2419
|
-
<span class="str">"world"</span>: world},
|
|
2420
|
-
timeout=0.5
|
|
2421
|
-
).json()
|
|
2422
|
-
<span class="kw">except</span>:
|
|
2423
|
-
v = {<span class="str">"decision"</span>: <span class="str">"ALLOW"</span>}
|
|
2424
|
-
<span class="kw">if</span> v[<span class="str">"decision"</span>] == <span class="str">"BLOCK"</span>:
|
|
2425
|
-
<span class="kw">return</span> f<span class="str">"Blocked: {v['reason']}"</span>
|
|
2426
|
-
<span class="kw">return</span> fn(*args, **kwargs)
|
|
2427
|
-
<span class="kw">return</span> wrapper
|
|
2428
|
-
|
|
2429
|
-
<span class="comment"># Usage: wrap any tool</span>
|
|
2430
|
-
@tool
|
|
2431
|
-
@governed
|
|
2432
|
-
<span class="kw">def</span> execute_trade(ticker, amount):
|
|
2433
|
-
...</div>
|
|
2434
|
-
</div>
|
|
2435
|
-
</div>
|
|
2436
|
-
|
|
2437
|
-
<div style="margin-top:8px">
|
|
2438
|
-
<div class="integrate-hint">
|
|
2439
|
-
<span style="display:inline-block;padding:2px 6px;background:#2d0606;color:#f87171;border-radius:3px;margin-right:3px">BLOCK</span> stopped
|
|
2440
|
-
<span style="display:inline-block;padding:2px 6px;background:#2d2006;color:#fbbf24;border-radius:3px;margin-right:3px;margin-left:4px">MODIFY</span> adjusted
|
|
2441
|
-
<span style="display:inline-block;padding:2px 6px;background:#052e16;color:#4ade80;border-radius:3px;margin-left:4px">ALLOW</span> proceeds
|
|
2442
|
-
</div>
|
|
2443
|
-
<div style="font-size:10px;color:var(--text-faint);margin-top:8px;border-top:1px solid var(--border);padding-top:6px">
|
|
2444
|
-
The world file controls everything. Same rules govern simulated and live systems.
|
|
2445
|
-
</div>
|
|
2446
|
-
</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>
|
|
2447
2929
|
</div>
|
|
2448
2930
|
|
|
2449
2931
|
<!-- Session Report Panel -->
|
|
2450
2932
|
<div class="ctrl-section" id="session-panel">
|
|
2451
2933
|
<h3 class="ctrl-label">SESSION</h3>
|
|
2452
|
-
<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">
|
|
2453
2935
|
<div class="metric-box"><div class="value" id="s-total">0</div><div class="label">Evaluations</div></div>
|
|
2454
2936
|
<div class="metric-box"><div class="value" id="s-blocked" style="color:#f87171">0</div><div class="label">Blocked</div></div>
|
|
2455
2937
|
<div class="metric-box"><div class="value" id="s-modified" style="color:#fbbf24">0</div><div class="label">Modified</div></div>
|
|
2456
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>
|
|
2457
2941
|
</div>
|
|
2458
2942
|
<div id="s-agents" style="font-size:10px;color:#888;margin-bottom:6px"></div>
|
|
2459
2943
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
@@ -2466,52 +2950,58 @@ actions = {}
|
|
|
2466
2950
|
</div>
|
|
2467
2951
|
</div>
|
|
2468
2952
|
|
|
2469
|
-
<!-- RIGHT:
|
|
2953
|
+
<!-- RIGHT: OBSERVATION DECK -->
|
|
2470
2954
|
<div class="viewer">
|
|
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>
|
|
2471
2959
|
|
|
2472
|
-
<!--
|
|
2473
|
-
<div class="
|
|
2474
|
-
<div class="
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
<div
|
|
2480
|
-
<div class="cc-value" id="cc-strength">--</div>
|
|
2481
|
-
</div>
|
|
2482
|
-
<div class="confidence-card">
|
|
2483
|
-
<div class="cc-label">Evidence Quality</div>
|
|
2484
|
-
<div class="cc-value" id="cc-evidence">--</div>
|
|
2485
|
-
</div>
|
|
2486
|
-
<div class="confidence-card">
|
|
2487
|
-
<div class="cc-label">Risk of Error</div>
|
|
2488
|
-
<div class="cc-value" id="cc-risk">--</div>
|
|
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>
|
|
2489
2968
|
</div>
|
|
2490
2969
|
</div>
|
|
2491
|
-
<div class="
|
|
2970
|
+
<div class="choice-round-label" id="choice-round-label" style="display:none">Round <span id="choice-round-num">1</span></div>
|
|
2492
2971
|
</div>
|
|
2493
2972
|
|
|
2494
|
-
<!--
|
|
2495
|
-
<div class="
|
|
2496
|
-
<
|
|
2497
|
-
|
|
2498
|
-
<
|
|
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>
|
|
2499
2978
|
</div>
|
|
2500
|
-
<div class="
|
|
2501
|
-
<
|
|
2979
|
+
<div class="choice-stream" id="choice-stream">
|
|
2980
|
+
<div class="stream-empty">Agent decisions will stream here in real time</div>
|
|
2502
2981
|
</div>
|
|
2503
|
-
<div class="activity-timeline" id="activity-timeline"></div>
|
|
2504
2982
|
</div>
|
|
2505
2983
|
|
|
2506
|
-
<!--
|
|
2507
|
-
<div class="
|
|
2508
|
-
<
|
|
2509
|
-
<div id="
|
|
2510
|
-
|
|
2511
|
-
|
|
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>
|
|
2512
2996
|
</div>
|
|
2513
2997
|
|
|
2514
|
-
<!--
|
|
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 -->
|
|
2515
3005
|
<div class="export-bar">
|
|
2516
3006
|
<button class="export-btn" onclick="exportPDF()">Download PDF</button>
|
|
2517
3007
|
<button class="export-btn" onclick="exportCSV()">Export CSV</button>
|
|
@@ -2555,27 +3045,65 @@ actions = {}
|
|
|
2555
3045
|
</div>
|
|
2556
3046
|
</div>
|
|
2557
3047
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
if (panel) panel.classList.add('active');
|
|
2569
|
-
}
|
|
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">
|
|
2570
3058
|
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
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>
|
|
3064
|
+
</div>
|
|
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.
|
|
3072
|
+
</div>
|
|
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>
|
|
3080
|
+
</div>
|
|
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>
|
|
3088
|
+
</div>
|
|
3089
|
+
</div>
|
|
2578
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>
|
|
3097
|
+
</div>
|
|
3098
|
+
</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>
|
|
3102
|
+
</div>
|
|
3103
|
+
</div>
|
|
3104
|
+
</div>
|
|
3105
|
+
|
|
3106
|
+
<script>
|
|
2579
3107
|
// ============================================
|
|
2580
3108
|
// OUTCOME INTELLIGENCE LAYER
|
|
2581
3109
|
// ============================================
|
|
@@ -2659,6 +3187,12 @@ function translateBehaviorNarrative(reaction, verdict) {
|
|
|
2659
3187
|
if (status === 'BLOCK') {
|
|
2660
3188
|
return 'abandoned ' + action + ' and switched to a safer strategy';
|
|
2661
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
|
+
}
|
|
2662
3196
|
if (status === 'MODIFY' || status === 'PAUSE') {
|
|
2663
3197
|
return 'scaled back ' + action + ' after early resistance';
|
|
2664
3198
|
}
|
|
@@ -2692,7 +3226,9 @@ function generateBehaviorShifts(shiftData, bLog) {
|
|
|
2692
3226
|
if (sentences.length === 0 && bLog.length > 0) {
|
|
2693
3227
|
var blocked = bLog.filter(function(e) { return e.status === 'BLOCK'; }).length;
|
|
2694
3228
|
var modified = bLog.filter(function(e) { return e.status === 'MODIFY' || e.status === 'PAUSE'; }).length;
|
|
2695
|
-
var
|
|
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;
|
|
2696
3232
|
|
|
2697
3233
|
if (blocked > 0) {
|
|
2698
3234
|
var bPct = Math.round((blocked / bLog.length) * 100);
|
|
@@ -2702,7 +3238,15 @@ function generateBehaviorShifts(shiftData, bLog) {
|
|
|
2702
3238
|
var mPct = Math.round((modified / bLog.length) * 100);
|
|
2703
3239
|
sentences.push(mPct + '% of agents (' + modified + ') reduced position size after initial attempts failed');
|
|
2704
3240
|
}
|
|
2705
|
-
if (
|
|
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)) {
|
|
2706
3250
|
var pPct = Math.round((proceeded / bLog.length) * 100);
|
|
2707
3251
|
sentences.push(pPct + '% of agents (' + proceeded + ') maintained their original strategy throughout');
|
|
2708
3252
|
}
|
|
@@ -2786,22 +3330,19 @@ let behaviorLog = []; // { agent, action, status, reason, ts }
|
|
|
2786
3330
|
let latestStability = 0;
|
|
2787
3331
|
let latestVolatility = 0;
|
|
2788
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: {...} }
|
|
3338
|
+
|
|
2789
3339
|
const statusEl = document.getElementById('status');
|
|
2790
|
-
const worldSelect = document.getElementById('world-select');
|
|
2791
3340
|
const stateVarsSection = document.getElementById('state-vars-section');
|
|
2792
3341
|
const stateVarsEl = document.getElementById('state-vars');
|
|
2793
|
-
const scenarioListEl = document.getElementById('scenario-list');
|
|
2794
|
-
const eventSelect = document.getElementById('event-select');
|
|
2795
|
-
const eventRoundInput = document.getElementById('event-round');
|
|
2796
|
-
const injectListEl = document.getElementById('inject-list');
|
|
2797
|
-
const roundsSlider = document.getElementById('rounds-slider');
|
|
2798
|
-
const roundsVal = document.getElementById('rounds-val');
|
|
2799
3342
|
const runBtn = document.getElementById('run-btn');
|
|
2800
3343
|
const agentsEl = document.getElementById('agents');
|
|
2801
3344
|
const logEl = document.getElementById('log');
|
|
2802
3345
|
const activeInvEl = document.getElementById('active-invariants');
|
|
2803
|
-
const engineSelect = document.getElementById('engine-select');
|
|
2804
|
-
const engineStatusEl = document.getElementById('engine-status');
|
|
2805
3346
|
const traceSourceEl = document.getElementById('trace-source');
|
|
2806
3347
|
const outcomeStatementEl = document.getElementById('outcome-statement');
|
|
2807
3348
|
const confidenceGridEl = document.getElementById('confidence-grid');
|
|
@@ -2861,79 +3402,50 @@ function exportCSV() {
|
|
|
2861
3402
|
}
|
|
2862
3403
|
|
|
2863
3404
|
function copyShareSummary() {
|
|
2864
|
-
var
|
|
2865
|
-
var shifts = [];
|
|
2866
|
-
behaviorShiftsEl.querySelectorAll('.behavior-shift-item').forEach(function(el) { shifts.push('- ' + el.textContent); });
|
|
2867
|
-
var causes = [];
|
|
2868
|
-
whyContentEl.querySelectorAll('.why-item').forEach(function(el) { causes.push('- ' + el.textContent); });
|
|
3405
|
+
var text = 'OBSERVATION DECK SUMMARY\\n\\n';
|
|
2869
3406
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
if (
|
|
2873
|
-
|
|
3407
|
+
// Drift verdict
|
|
3408
|
+
var driftVerdict = document.getElementById('drift-verdict');
|
|
3409
|
+
if (driftVerdict && driftVerdict.textContent) {
|
|
3410
|
+
text += 'BEHAVIORAL DRIFT: ' + driftVerdict.textContent + '\\n\\n';
|
|
3411
|
+
}
|
|
2874
3412
|
|
|
2875
|
-
|
|
2876
|
-
|
|
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');
|
|
2877
3426
|
});
|
|
2878
3427
|
}
|
|
2879
3428
|
|
|
2880
3429
|
// ============================================
|
|
2881
|
-
// INIT — Load
|
|
3430
|
+
// INIT — Load bridge capabilities and saved rules
|
|
2882
3431
|
// ============================================
|
|
2883
3432
|
async function init() {
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
fetch('/api/
|
|
2887
|
-
fetch('/api/
|
|
2888
|
-
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: [] })),
|
|
2889
3437
|
]);
|
|
2890
3438
|
|
|
2891
3439
|
worlds = wRes.worlds;
|
|
2892
|
-
scenarios = sRes.scenarios;
|
|
2893
|
-
narratives = nRes.narratives;
|
|
2894
|
-
|
|
2895
|
-
// Populate engine selector with live adapters
|
|
2896
|
-
(aRes.adapters || []).forEach(function(a) {
|
|
2897
|
-
const opt = document.createElement('option');
|
|
2898
|
-
opt.value = a.id;
|
|
2899
|
-
opt.textContent = a.label;
|
|
2900
|
-
engineSelect.appendChild(opt);
|
|
2901
|
-
});
|
|
2902
3440
|
|
|
2903
|
-
//
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
opt.value = w.id;
|
|
2907
|
-
opt.textContent = w.title;
|
|
2908
|
-
worldSelect.appendChild(opt);
|
|
3441
|
+
// Index bridge capabilities
|
|
3442
|
+
(bcRes.bridges || []).forEach(function(b) {
|
|
3443
|
+
bridgeCapabilities[b.id] = b;
|
|
2909
3444
|
});
|
|
2910
3445
|
|
|
2911
|
-
//
|
|
2912
|
-
Object.entries(narratives).forEach(([id, ev]) => {
|
|
2913
|
-
const opt = document.createElement('option');
|
|
2914
|
-
opt.value = id;
|
|
2915
|
-
opt.textContent = ev.headline.slice(0, 40);
|
|
2916
|
-
eventSelect.appendChild(opt);
|
|
2917
|
-
});
|
|
2918
|
-
|
|
2919
|
-
// Populate scenario presets
|
|
2920
|
-
Object.entries(scenarios).forEach(([id, s]) => {
|
|
2921
|
-
const btn = document.createElement('button');
|
|
2922
|
-
btn.className = 'scenario-btn';
|
|
2923
|
-
btn.innerHTML = '<div class="stitle">' + s.title + '</div><div class="sdesc">' + s.description + '</div>';
|
|
2924
|
-
btn.onclick = () => loadScenario(id, s);
|
|
2925
|
-
scenarioListEl.appendChild(btn);
|
|
2926
|
-
});
|
|
2927
|
-
|
|
2928
|
-
// Select first world
|
|
2929
|
-
if (worlds.length > 0) selectWorld(worlds[0].id);
|
|
2930
|
-
|
|
2931
|
-
// Load saved variants
|
|
3446
|
+
// Load saved rules
|
|
2932
3447
|
await loadVariants();
|
|
2933
3448
|
|
|
2934
|
-
// Populate base world selector for custom rules mode
|
|
2935
|
-
populateBaseWorldSelect();
|
|
2936
|
-
|
|
2937
3449
|
// Connect SSE
|
|
2938
3450
|
connectSSE();
|
|
2939
3451
|
}
|
|
@@ -2942,110 +3454,384 @@ function selectWorld(worldId) {
|
|
|
2942
3454
|
currentWorld = worlds.find(w => w.id === worldId);
|
|
2943
3455
|
if (!currentWorld) return;
|
|
2944
3456
|
|
|
2945
|
-
|
|
2946
|
-
|
|
3457
|
+
// Render state variable controls if world has them
|
|
3458
|
+
renderStateVars();
|
|
3459
|
+
}
|
|
2947
3460
|
|
|
2948
|
-
|
|
2949
|
-
if (currentWorld.stateVariables
|
|
2950
|
-
stateVarsSection.style.display = '';
|
|
2951
|
-
stateVarsEl.innerHTML = '';
|
|
2952
|
-
currentWorld.stateVariables.forEach(sv => {
|
|
2953
|
-
const row = document.createElement('div');
|
|
2954
|
-
row.className = 'ctrl-row';
|
|
2955
|
-
|
|
2956
|
-
if (sv.type === 'number' && sv.range) {
|
|
2957
|
-
const step = sv.range.max <= 1 ? 0.01 : (sv.range.max <= 10 ? 0.1 : 1);
|
|
2958
|
-
row.innerHTML =
|
|
2959
|
-
'<div class="ctrl-label"><span>' + sv.label + '</span><span class="val" id="sv-val-' + sv.id + '">' + sv.default_value + '</span></div>' +
|
|
2960
|
-
'<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 + '">';
|
|
2961
|
-
stateVarsEl.appendChild(row);
|
|
2962
|
-
const slider = row.querySelector('input');
|
|
2963
|
-
slider.addEventListener('input', () => {
|
|
2964
|
-
document.getElementById('sv-val-' + sv.id).textContent = slider.value;
|
|
2965
|
-
});
|
|
2966
|
-
} else if (sv.type === 'enum' && sv.enum_values) {
|
|
2967
|
-
row.innerHTML =
|
|
2968
|
-
'<div class="ctrl-label"><span>' + sv.label + '</span></div>' +
|
|
2969
|
-
'<select id="sv-' + sv.id + '" data-sv="' + sv.id + '">' +
|
|
2970
|
-
sv.enum_values.map(v => '<option value="' + v + '"' + (v === sv.default_value ? ' selected' : '') + '>' + v + '</option>').join('') +
|
|
2971
|
-
'</select>';
|
|
2972
|
-
stateVarsEl.appendChild(row);
|
|
2973
|
-
} else if (sv.type === 'boolean') {
|
|
2974
|
-
row.innerHTML =
|
|
2975
|
-
'<div class="toggle-row">' +
|
|
2976
|
-
'<div class="toggle' + (sv.default_value ? ' on' : '') + '" id="sv-' + sv.id + '" data-sv="' + sv.id + '"></div>' +
|
|
2977
|
-
'<span class="toggle-label">' + sv.label + '</span>' +
|
|
2978
|
-
'</div>';
|
|
2979
|
-
stateVarsEl.appendChild(row);
|
|
2980
|
-
const toggle = row.querySelector('.toggle');
|
|
2981
|
-
toggle.addEventListener('click', () => {
|
|
2982
|
-
toggle.classList.toggle('on');
|
|
2983
|
-
});
|
|
2984
|
-
}
|
|
2985
|
-
});
|
|
2986
|
-
} else {
|
|
3461
|
+
function renderStateVars() {
|
|
3462
|
+
if (!currentWorld || !currentWorld.stateVariables || currentWorld.stateVariables.length === 0) {
|
|
2987
3463
|
stateVarsSection.style.display = 'none';
|
|
3464
|
+
return;
|
|
2988
3465
|
}
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
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
|
+
});
|
|
2996
3504
|
}
|
|
2997
3505
|
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
}
|
|
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
|
+
});
|
|
3009
3518
|
|
|
3010
3519
|
// ============================================
|
|
3011
|
-
//
|
|
3520
|
+
// THESIS INPUT — Generate Governance Rules
|
|
3012
3521
|
// ============================================
|
|
3013
|
-
document.getElementById('
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
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;
|
|
3020
3530
|
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3531
|
+
currentThesis = thesis;
|
|
3532
|
+
generateRulesBtn.disabled = true;
|
|
3533
|
+
generateRulesBtn.textContent = 'Generating...';
|
|
3534
|
+
ruleGenStatusEl.textContent = '';
|
|
3535
|
+
|
|
3536
|
+
try {
|
|
3537
|
+
const resp = await fetch('/api/generate-rules', {
|
|
3538
|
+
method: 'POST',
|
|
3539
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3540
|
+
body: JSON.stringify({ thesis }),
|
|
3029
3541
|
});
|
|
3030
|
-
|
|
3031
|
-
|
|
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
|
+
});
|
|
3588
|
+
|
|
3589
|
+
// ============================================
|
|
3590
|
+
// WORLD FILE TOGGLE
|
|
3591
|
+
// ============================================
|
|
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
|
+
});
|
|
3032
3603
|
|
|
3033
3604
|
// ============================================
|
|
3034
|
-
//
|
|
3605
|
+
// BRIDGE CONNECTION
|
|
3035
3606
|
// ============================================
|
|
3036
|
-
|
|
3037
|
-
|
|
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
|
+
}
|
|
3038
3818
|
|
|
3039
3819
|
// ============================================
|
|
3040
|
-
// RUN
|
|
3820
|
+
// RUN GOVERNANCE
|
|
3041
3821
|
// ============================================
|
|
3042
3822
|
runBtn.addEventListener('click', async () => {
|
|
3043
|
-
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
|
+
|
|
3044
3829
|
runBtn.disabled = true;
|
|
3045
3830
|
runBtn.textContent = 'Running...';
|
|
3046
3831
|
|
|
3047
3832
|
// Reset viewer state
|
|
3048
3833
|
totalInterventions = 0;
|
|
3834
|
+
totalActions = 0;
|
|
3049
3835
|
agentsEl.innerHTML = '';
|
|
3050
3836
|
logEl.innerHTML = '';
|
|
3051
3837
|
document.getElementById('m-stability').textContent = '--';
|
|
@@ -3053,9 +3839,9 @@ runBtn.addEventListener('click', async () => {
|
|
|
3053
3839
|
document.getElementById('m-round').textContent = '--';
|
|
3054
3840
|
document.getElementById('m-interventions').textContent = '0';
|
|
3055
3841
|
|
|
3056
|
-
// Gather state overrides
|
|
3842
|
+
// Gather state overrides from sliders (if world has them)
|
|
3057
3843
|
const stateOverrides = {};
|
|
3058
|
-
if (currentWorld.stateVariables) {
|
|
3844
|
+
if (currentWorld && currentWorld.stateVariables) {
|
|
3059
3845
|
currentWorld.stateVariables.forEach(sv => {
|
|
3060
3846
|
const el = document.getElementById('sv-' + sv.id);
|
|
3061
3847
|
if (!el) return;
|
|
@@ -3065,39 +3851,36 @@ runBtn.addEventListener('click', async () => {
|
|
|
3065
3851
|
});
|
|
3066
3852
|
}
|
|
3067
3853
|
|
|
3068
|
-
const selectedEngine = engineSelect.value;
|
|
3069
|
-
|
|
3070
3854
|
try {
|
|
3071
|
-
if (
|
|
3072
|
-
//
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3855
|
+
if (connectedBridge) {
|
|
3856
|
+
// Run via bridge adapter
|
|
3857
|
+
const payload = {
|
|
3858
|
+
adapterId: connectedBridge.id,
|
|
3859
|
+
worldId: currentWorld ? currentWorld.id : 'social_simulation',
|
|
3075
3860
|
stateOverrides,
|
|
3076
|
-
injectEvents: injectedEvents.length > 0 ? injectedEvents : undefined,
|
|
3077
|
-
rounds: parseInt(roundsSlider.value),
|
|
3078
3861
|
};
|
|
3079
|
-
await fetch('/api/run-
|
|
3862
|
+
await fetch('/api/run-live', {
|
|
3080
3863
|
method: 'POST',
|
|
3081
3864
|
headers: { 'Content-Type': 'application/json' },
|
|
3082
|
-
body: JSON.stringify(
|
|
3865
|
+
body: JSON.stringify(payload),
|
|
3083
3866
|
});
|
|
3084
3867
|
} else {
|
|
3085
|
-
//
|
|
3086
|
-
const
|
|
3087
|
-
|
|
3088
|
-
worldId: currentWorld.id,
|
|
3868
|
+
// Fallback: run internal simulation with generated rules
|
|
3869
|
+
const config = {
|
|
3870
|
+
worldId: currentWorld ? currentWorld.id : 'social_simulation',
|
|
3089
3871
|
stateOverrides,
|
|
3872
|
+
rounds: 5,
|
|
3090
3873
|
};
|
|
3091
|
-
await fetch('/api/run-
|
|
3874
|
+
await fetch('/api/run-sim', {
|
|
3092
3875
|
method: 'POST',
|
|
3093
3876
|
headers: { 'Content-Type': 'application/json' },
|
|
3094
|
-
body: JSON.stringify(
|
|
3877
|
+
body: JSON.stringify(config),
|
|
3095
3878
|
});
|
|
3096
3879
|
}
|
|
3097
3880
|
} catch (err) {
|
|
3098
|
-
addLog('Error starting
|
|
3881
|
+
addLog('Error starting governance: ' + err.message, 'block');
|
|
3099
3882
|
runBtn.disabled = false;
|
|
3100
|
-
runBtn.textContent = 'Run
|
|
3883
|
+
runBtn.textContent = 'Run Governance';
|
|
3101
3884
|
}
|
|
3102
3885
|
});
|
|
3103
3886
|
|
|
@@ -3225,9 +4008,13 @@ function renderAgents(reactions) {
|
|
|
3225
4008
|
}
|
|
3226
4009
|
|
|
3227
4010
|
// ============================================
|
|
3228
|
-
//
|
|
4011
|
+
// OBSERVATION DECK — Rendering
|
|
3229
4012
|
// ============================================
|
|
3230
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
|
+
|
|
3231
4018
|
function recordBehavior(reactions, round) {
|
|
3232
4019
|
if (!reactions || !reactions.length) return;
|
|
3233
4020
|
reactions.forEach(function(r) {
|
|
@@ -3244,62 +4031,283 @@ function recordBehavior(reactions, round) {
|
|
|
3244
4031
|
});
|
|
3245
4032
|
}
|
|
3246
4033
|
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
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);
|
|
3265
4055
|
}
|
|
3266
4056
|
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
var
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
}
|
|
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
|
+
}
|
|
3276
4068
|
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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;
|
|
3286
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;
|
|
3287
4178
|
}
|
|
3288
4179
|
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
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) + '%)');
|
|
3296
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>';
|
|
3297
4305
|
}
|
|
3298
4306
|
|
|
4307
|
+
// ── Master update ──
|
|
3299
4308
|
function updateAllPanels() {
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
updateWhyPanel();
|
|
4309
|
+
// Observation Deck updates are driven by renderChoiceBubbles/renderDecisionFlow
|
|
4310
|
+
// called directly from handleEvent — this function exists for compatibility
|
|
3303
4311
|
}
|
|
3304
4312
|
|
|
3305
4313
|
function handleEvent(event) {
|
|
@@ -3307,18 +4315,34 @@ function handleEvent(event) {
|
|
|
3307
4315
|
statusEl.className = 'status live';
|
|
3308
4316
|
statusEl.textContent = 'LIVE';
|
|
3309
4317
|
// Show simulation source
|
|
3310
|
-
const src = event.source || '
|
|
3311
|
-
if (src !== 'nv-sim') {
|
|
4318
|
+
const src = event.source || 'governance';
|
|
4319
|
+
if (src !== 'nv-sim' && src !== 'governance') {
|
|
3312
4320
|
traceSourceEl.textContent = '● ' + src.toUpperCase() + ' (LIVE)';
|
|
3313
4321
|
traceSourceEl.style.color = '#4ade80';
|
|
3314
|
-
engineStatusEl.textContent = 'Streaming from ' + src;
|
|
3315
|
-
engineStatusEl.style.color = '#4ade80';
|
|
3316
4322
|
} else {
|
|
3317
4323
|
traceSourceEl.textContent = '';
|
|
3318
|
-
engineStatusEl.textContent = '';
|
|
3319
4324
|
}
|
|
3320
4325
|
addLog('Simulation started: ' + event.agents.length + ' agents, ' + event.totalRounds + ' rounds' + (src !== 'nv-sim' ? ' [source: ' + src + ']' : ''));
|
|
3321
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';
|
|
3322
4346
|
// Store narrative events by round for trace rendering
|
|
3323
4347
|
narrativeEventsByRound = {};
|
|
3324
4348
|
(event.narrativeEvents || []).forEach(function(ev) {
|
|
@@ -3407,11 +4431,25 @@ function handleEvent(event) {
|
|
|
3407
4431
|
document.getElementById('m-volatility').textContent = (event.maxVolatility * 100).toFixed(0) + '%';
|
|
3408
4432
|
document.getElementById('m-interventions').textContent = totalInterventions;
|
|
3409
4433
|
|
|
3410
|
-
// Track system shifts (data collection
|
|
4434
|
+
// Track system shifts (data collection)
|
|
3411
4435
|
trackShift(event);
|
|
3412
4436
|
|
|
3413
|
-
//
|
|
3414
|
-
|
|
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
|
+
}
|
|
3415
4453
|
|
|
3416
4454
|
// Add trace entry for audit
|
|
3417
4455
|
addTraceRound(event);
|
|
@@ -3436,7 +4474,12 @@ function handleEvent(event) {
|
|
|
3436
4474
|
renderSystemShift(r);
|
|
3437
4475
|
renderRuleImpacts(r);
|
|
3438
4476
|
renderEnforcementClassification(r.enforcementClassification || []);
|
|
3439
|
-
|
|
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);
|
|
3440
4483
|
lastSimResult = {
|
|
3441
4484
|
stability: r.governed.metrics.stabilityScore,
|
|
3442
4485
|
volatility: r.governed.metrics.maxVolatility,
|
|
@@ -3445,7 +4488,88 @@ function handleEvent(event) {
|
|
|
3445
4488
|
};
|
|
3446
4489
|
}
|
|
3447
4490
|
runBtn.disabled = false;
|
|
3448
|
-
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('');
|
|
3449
4573
|
}
|
|
3450
4574
|
}
|
|
3451
4575
|
|
|
@@ -3720,11 +4844,11 @@ cancelSaveBtn.addEventListener('click', () => {
|
|
|
3720
4844
|
|
|
3721
4845
|
confirmSaveBtn.addEventListener('click', async () => {
|
|
3722
4846
|
const name = variantNameInput.value.trim();
|
|
3723
|
-
if (!name
|
|
4847
|
+
if (!name) return;
|
|
3724
4848
|
|
|
3725
4849
|
// Gather current state overrides
|
|
3726
4850
|
const stateOverrides = {};
|
|
3727
|
-
if (currentWorld.stateVariables) {
|
|
4851
|
+
if (currentWorld && currentWorld.stateVariables) {
|
|
3728
4852
|
currentWorld.stateVariables.forEach(sv => {
|
|
3729
4853
|
const el = document.getElementById('sv-' + sv.id);
|
|
3730
4854
|
if (!el) return;
|
|
@@ -3734,13 +4858,17 @@ confirmSaveBtn.addEventListener('click', async () => {
|
|
|
3734
4858
|
});
|
|
3735
4859
|
}
|
|
3736
4860
|
|
|
4861
|
+
// Save as SimulationRules (complete snapshot)
|
|
3737
4862
|
const payload = {
|
|
3738
4863
|
name,
|
|
3739
4864
|
description: variantDescInput.value.trim(),
|
|
3740
|
-
|
|
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,
|
|
3741
4871
|
stateOverrides,
|
|
3742
|
-
events: injectedEvents.slice(),
|
|
3743
|
-
rounds: parseInt(roundsSlider.value),
|
|
3744
4872
|
lastResult: lastSimResult,
|
|
3745
4873
|
};
|
|
3746
4874
|
|
|
@@ -3773,18 +4901,21 @@ async function loadVariants() {
|
|
|
3773
4901
|
|
|
3774
4902
|
function renderVariants(variants) {
|
|
3775
4903
|
if (variants.length === 0) {
|
|
3776
|
-
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>';
|
|
3777
4905
|
return;
|
|
3778
4906
|
}
|
|
3779
4907
|
variantListEl.innerHTML = variants.map(v => {
|
|
3780
4908
|
const resultHtml = v.lastResult
|
|
3781
4909
|
? '<span class="vresult">Stability: ' + (v.lastResult.stability * 100).toFixed(0) + '% | Effectiveness: ' + (v.lastResult.governanceEffectiveness * 100).toFixed(0) + '%</span>'
|
|
3782
|
-
: '<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>' : '');
|
|
3783
4913
|
return '<div class="variant-card" data-vid="' + v.id + '">' +
|
|
3784
4914
|
'<div class="vname">' + v.name + '</div>' +
|
|
4915
|
+
(v.thesis ? '<div class="vdesc" style="font-style:italic">' + v.thesis + '</div>' : '') +
|
|
3785
4916
|
(v.description ? '<div class="vdesc">' + v.description + '</div>' : '') +
|
|
3786
|
-
|
|
3787
|
-
'<div class="vmeta">' + resultHtml + ' | ' +
|
|
4917
|
+
bridgeLabel +
|
|
4918
|
+
'<div class="vmeta">' + resultHtml + (rulesCount ? ' | ' + rulesCount : '') + '</div>' +
|
|
3788
4919
|
'<span class="vdelete" data-vid="' + v.id + '">delete</span>' +
|
|
3789
4920
|
'</div>';
|
|
3790
4921
|
}).join('');
|
|
@@ -3815,38 +4946,49 @@ function renderVariants(variants) {
|
|
|
3815
4946
|
}
|
|
3816
4947
|
|
|
3817
4948
|
function loadVariant(variant) {
|
|
3818
|
-
//
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
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('');
|
|
3840
4974
|
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
4975
|
+
ruleGenStatusEl.textContent = variant.rules.length + ' rule(s) loaded from "' + variant.name + '".';
|
|
4976
|
+
ruleGenStatusEl.className = 'rule-status success';
|
|
4977
|
+
}
|
|
3844
4978
|
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
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
|
+
}
|
|
3848
4990
|
|
|
3849
|
-
addLog('Loaded
|
|
4991
|
+
addLog('Loaded rules: ' + variant.name, '');
|
|
3850
4992
|
}
|
|
3851
4993
|
|
|
3852
4994
|
// ============================================
|
|
@@ -3889,129 +5031,15 @@ const savedTheme = localStorage.getItem('nv-theme');
|
|
|
3889
5031
|
if (savedTheme) applyTheme(savedTheme);
|
|
3890
5032
|
|
|
3891
5033
|
// ============================================
|
|
3892
|
-
//
|
|
5034
|
+
// (Old world source switching removed — replaced by thesis + bridge flow)
|
|
3893
5035
|
// ============================================
|
|
3894
|
-
let currentWorldSource = 'preset';
|
|
3895
|
-
const worldSourceTabs = document.querySelectorAll('.ws-tab');
|
|
3896
|
-
const sourcePresetPanel = document.getElementById('source-preset');
|
|
3897
|
-
const sourceCustomPanel = document.getElementById('source-custom');
|
|
3898
|
-
const sourceUploadPanel = document.getElementById('source-upload');
|
|
3899
|
-
|
|
3900
|
-
worldSourceTabs.forEach(tab => {
|
|
3901
|
-
tab.addEventListener('click', () => {
|
|
3902
|
-
const source = tab.dataset.source;
|
|
3903
|
-
if (source === currentWorldSource) return;
|
|
3904
|
-
|
|
3905
|
-
currentWorldSource = source;
|
|
3906
|
-
|
|
3907
|
-
// Update tab visuals
|
|
3908
|
-
worldSourceTabs.forEach(t => t.classList.remove('active'));
|
|
3909
|
-
tab.classList.add('active');
|
|
3910
|
-
tab.querySelector('input').checked = true;
|
|
3911
|
-
|
|
3912
|
-
// Show/hide panels
|
|
3913
|
-
sourcePresetPanel.style.display = source === 'preset' ? '' : 'none';
|
|
3914
|
-
sourceCustomPanel.style.display = source === 'custom' ? '' : 'none';
|
|
3915
|
-
sourceUploadPanel.style.display = source === 'upload' ? '' : 'none';
|
|
3916
|
-
});
|
|
3917
|
-
});
|
|
3918
|
-
|
|
3919
|
-
// Populate base world selector in custom rules panel
|
|
3920
|
-
function populateBaseWorldSelect() {
|
|
3921
|
-
const select = document.getElementById('custom-base-world');
|
|
3922
|
-
if (!select) return;
|
|
3923
|
-
worlds.forEach(w => {
|
|
3924
|
-
const opt = document.createElement('option');
|
|
3925
|
-
opt.value = w.id;
|
|
3926
|
-
opt.textContent = w.title;
|
|
3927
|
-
select.appendChild(opt);
|
|
3928
|
-
});
|
|
3929
|
-
}
|
|
3930
5036
|
|
|
3931
5037
|
// ============================================
|
|
3932
|
-
//
|
|
5038
|
+
// (Old world action bar removed — replaced by thesis + bridge flow)
|
|
3933
5039
|
// ============================================
|
|
3934
5040
|
|
|
3935
|
-
// + New World
|
|
3936
|
-
document.getElementById('new-world-btn').addEventListener('click', () => {
|
|
3937
|
-
// Switch to custom rules mode
|
|
3938
|
-
currentWorldSource = 'custom';
|
|
3939
|
-
worldSourceTabs.forEach(t => {
|
|
3940
|
-
t.classList.toggle('active', t.dataset.source === 'custom');
|
|
3941
|
-
t.querySelector('input').checked = t.dataset.source === 'custom';
|
|
3942
|
-
});
|
|
3943
|
-
sourcePresetPanel.style.display = 'none';
|
|
3944
|
-
sourceCustomPanel.style.display = '';
|
|
3945
|
-
sourceUploadPanel.style.display = 'none';
|
|
3946
|
-
|
|
3947
|
-
// Clear everything
|
|
3948
|
-
document.getElementById('custom-world-name').value = '';
|
|
3949
|
-
document.getElementById('custom-world-thesis').value = '';
|
|
3950
|
-
document.getElementById('rule-input').value = '';
|
|
3951
|
-
document.getElementById('parsed-rules').innerHTML = '';
|
|
3952
|
-
document.getElementById('rule-status').textContent = '';
|
|
3953
|
-
document.getElementById('rule-status').className = 'rule-status';
|
|
3954
|
-
document.getElementById('custom-base-world').value = '';
|
|
3955
|
-
|
|
3956
|
-
// Clear active rules server-side
|
|
3957
|
-
fetch('/api/clear-rules', { method: 'POST' });
|
|
3958
|
-
|
|
3959
|
-
// Reset right panel
|
|
3960
|
-
document.getElementById('active-invariants').innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No rules loaded. Define your world.</div>';
|
|
3961
|
-
});
|
|
3962
|
-
|
|
3963
|
-
// Clear Rules
|
|
3964
|
-
document.getElementById('clear-rules-btn').addEventListener('click', async () => {
|
|
3965
|
-
await fetch('/api/clear-rules', { method: 'POST' });
|
|
3966
|
-
|
|
3967
|
-
// Clear rule editor UI
|
|
3968
|
-
const ruleInput = document.getElementById('rule-input');
|
|
3969
|
-
if (ruleInput) ruleInput.value = '';
|
|
3970
|
-
const parsed = document.getElementById('parsed-rules');
|
|
3971
|
-
if (parsed) parsed.innerHTML = '';
|
|
3972
|
-
const status = document.getElementById('rule-status');
|
|
3973
|
-
if (status) { status.textContent = 'Rules cleared.'; status.className = 'rule-status success'; }
|
|
3974
|
-
|
|
3975
|
-
// Clear upload state
|
|
3976
|
-
const uploadStatus = document.getElementById('upload-status');
|
|
3977
|
-
if (uploadStatus) { uploadStatus.textContent = 'Rules cleared.'; uploadStatus.className = 'rule-status success'; }
|
|
3978
|
-
const loadedInfo = document.getElementById('loaded-world-info');
|
|
3979
|
-
if (loadedInfo) loadedInfo.style.display = 'none';
|
|
3980
|
-
});
|
|
3981
|
-
|
|
3982
|
-
// Load World File (switch to upload tab)
|
|
3983
|
-
document.getElementById('load-file-btn').addEventListener('click', () => {
|
|
3984
|
-
currentWorldSource = 'upload';
|
|
3985
|
-
worldSourceTabs.forEach(t => {
|
|
3986
|
-
t.classList.toggle('active', t.dataset.source === 'upload');
|
|
3987
|
-
t.querySelector('input').checked = t.dataset.source === 'upload';
|
|
3988
|
-
});
|
|
3989
|
-
sourcePresetPanel.style.display = 'none';
|
|
3990
|
-
sourceCustomPanel.style.display = 'none';
|
|
3991
|
-
sourceUploadPanel.style.display = '';
|
|
3992
|
-
});
|
|
3993
|
-
|
|
3994
|
-
// Save as World File (export)
|
|
3995
|
-
document.getElementById('export-world-btn').addEventListener('click', async () => {
|
|
3996
|
-
try {
|
|
3997
|
-
const resp = await fetch('/api/export-world');
|
|
3998
|
-
const data = await resp.json();
|
|
3999
|
-
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
4000
|
-
const url = URL.createObjectURL(blob);
|
|
4001
|
-
const a = document.createElement('a');
|
|
4002
|
-
a.href = url;
|
|
4003
|
-
a.download = (currentWorld ? currentWorld.id : 'custom') + '-world.json';
|
|
4004
|
-
document.body.appendChild(a);
|
|
4005
|
-
a.click();
|
|
4006
|
-
document.body.removeChild(a);
|
|
4007
|
-
URL.revokeObjectURL(url);
|
|
4008
|
-
} catch (err) {
|
|
4009
|
-
alert('Export failed: ' + err.message);
|
|
4010
|
-
}
|
|
4011
|
-
});
|
|
4012
|
-
|
|
4013
5041
|
// ============================================
|
|
4014
|
-
// WORLD FILE UPLOAD / PASTE
|
|
5042
|
+
// WORLD FILE UPLOAD / PASTE (simplified)
|
|
4015
5043
|
// ============================================
|
|
4016
5044
|
const uploadZone = document.getElementById('upload-zone');
|
|
4017
5045
|
const uploadFileInput = document.getElementById('upload-file-input');
|
|
@@ -4020,56 +5048,43 @@ const worldJsonInput = document.getElementById('world-json-input');
|
|
|
4020
5048
|
const loadWorldBtn = document.getElementById('load-world-btn');
|
|
4021
5049
|
const uploadStatusEl = document.getElementById('upload-status');
|
|
4022
5050
|
|
|
4023
|
-
|
|
4024
|
-
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
|
|
4025
|
-
uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
|
|
4026
|
-
uploadZone.addEventListener('drop', (e) => {
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
});
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadFileInput.click(); });
|
|
4035
|
-
uploadFileInput.addEventListener('change', () => {
|
|
4036
|
-
if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]);
|
|
4037
|
-
});
|
|
4038
|
-
|
|
4039
|
-
// Click zone to browse
|
|
4040
|
-
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]); });
|
|
4041
5064
|
|
|
4042
5065
|
function readWorldFile(file) {
|
|
4043
5066
|
const reader = new FileReader();
|
|
4044
5067
|
reader.onload = (e) => {
|
|
4045
|
-
worldJsonInput.value = e.target.result;
|
|
4046
|
-
uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".';
|
|
4047
|
-
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'; }
|
|
4048
5070
|
};
|
|
4049
5071
|
reader.readAsText(file);
|
|
4050
5072
|
}
|
|
4051
5073
|
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
const jsonText = worldJsonInput.value.trim();
|
|
5074
|
+
if (loadWorldBtn) loadWorldBtn.addEventListener('click', async () => {
|
|
5075
|
+
const jsonText = worldJsonInput ? worldJsonInput.value.trim() : '';
|
|
4055
5076
|
if (!jsonText) {
|
|
4056
|
-
uploadStatusEl.textContent = 'Paste or upload a world file first.';
|
|
4057
|
-
uploadStatusEl.className = 'rule-status error';
|
|
5077
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = 'Paste or upload a world file first.'; uploadStatusEl.className = 'rule-status error'; }
|
|
4058
5078
|
return;
|
|
4059
5079
|
}
|
|
4060
5080
|
|
|
4061
5081
|
let worldData;
|
|
4062
|
-
try {
|
|
4063
|
-
|
|
4064
|
-
} catch (err) {
|
|
4065
|
-
uploadStatusEl.textContent = 'Invalid JSON: ' + err.message;
|
|
4066
|
-
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'; }
|
|
4067
5084
|
return;
|
|
4068
5085
|
}
|
|
4069
5086
|
|
|
4070
|
-
// Normalize: if the JSON is { world: {...} } or just {...}
|
|
4071
5087
|
const worldPayload = worldData.world || worldData;
|
|
4072
|
-
|
|
4073
5088
|
loadWorldBtn.textContent = 'Loading...';
|
|
4074
5089
|
loadWorldBtn.disabled = true;
|
|
4075
5090
|
|
|
@@ -4082,202 +5097,50 @@ loadWorldBtn.addEventListener('click', async () => {
|
|
|
4082
5097
|
const result = await resp.json();
|
|
4083
5098
|
|
|
4084
5099
|
if (result.error) {
|
|
4085
|
-
uploadStatusEl.textContent = result.error;
|
|
4086
|
-
uploadStatusEl.className = 'rule-status error';
|
|
5100
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = result.error; uploadStatusEl.className = 'rule-status error'; }
|
|
4087
5101
|
} else {
|
|
4088
|
-
uploadStatusEl.textContent = result.message;
|
|
4089
|
-
uploadStatusEl.className = 'rule-status success';
|
|
5102
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = result.message; uploadStatusEl.className = 'rule-status success'; }
|
|
4090
5103
|
|
|
4091
5104
|
// Show loaded world info
|
|
4092
|
-
|
|
4093
|
-
infoEl.style.display = '';
|
|
4094
|
-
document.getElementById('lw-name')
|
|
4095
|
-
|
|
4096
|
-
document.getElementById('lw-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
//
|
|
4102
|
-
|
|
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 =>
|
|
4103
5133
|
'<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
|
|
4104
|
-
).join('') + result.world.gates.map(g =>
|
|
4105
|
-
'<div class="inv-item" style="color:' + (g.severity === 'critical' ? 'var(--red)' : 'var(--yellow)') + '">[' + g.id + '] ' + g.label + '</div>'
|
|
4106
5134
|
).join('');
|
|
4107
|
-
activeInvEl.innerHTML = invHtml || '<div style="font-size:11px;color:var(--text-muted)">No invariants defined</div>';
|
|
4108
|
-
|
|
4109
|
-
// Update state variables if present
|
|
4110
|
-
if (result.world.stateVariables && result.world.stateVariables.length > 0) {
|
|
4111
|
-
// Store as a pseudo-world so sliders render
|
|
4112
|
-
currentWorld = {
|
|
4113
|
-
id: 'custom-world',
|
|
4114
|
-
title: result.world.title,
|
|
4115
|
-
thesis: result.world.thesis,
|
|
4116
|
-
stateVariables: result.world.stateVariables,
|
|
4117
|
-
invariants: result.world.invariants,
|
|
4118
|
-
gates: result.world.gates,
|
|
4119
|
-
};
|
|
4120
|
-
selectWorld('custom-world');
|
|
4121
|
-
} else {
|
|
4122
|
-
// Just set current world reference
|
|
4123
|
-
currentWorld = {
|
|
4124
|
-
id: 'custom-world',
|
|
4125
|
-
title: result.world.title,
|
|
4126
|
-
thesis: result.world.thesis,
|
|
4127
|
-
stateVariables: [],
|
|
4128
|
-
invariants: result.world.invariants,
|
|
4129
|
-
gates: result.world.gates,
|
|
4130
|
-
};
|
|
4131
|
-
}
|
|
4132
5135
|
}
|
|
4133
5136
|
} catch (err) {
|
|
4134
|
-
uploadStatusEl.textContent = 'Error: ' + err.message;
|
|
4135
|
-
uploadStatusEl.className = 'rule-status error';
|
|
5137
|
+
if (uploadStatusEl) { uploadStatusEl.textContent = 'Error: ' + err.message; uploadStatusEl.className = 'rule-status error'; }
|
|
4136
5138
|
}
|
|
4137
5139
|
|
|
4138
5140
|
loadWorldBtn.textContent = 'Load into Runtime';
|
|
4139
5141
|
loadWorldBtn.disabled = false;
|
|
4140
5142
|
});
|
|
4141
5143
|
|
|
4142
|
-
// ============================================
|
|
4143
|
-
// PLAIN-ENGLISH RULE EDITOR
|
|
4144
|
-
// ============================================
|
|
4145
|
-
const ruleInput = document.getElementById('rule-input');
|
|
4146
|
-
const parseRulesBtn = document.getElementById('parse-rules-btn');
|
|
4147
|
-
const parsedRulesEl = document.getElementById('parsed-rules');
|
|
4148
|
-
const ruleStatusEl = document.getElementById('rule-status');
|
|
4149
|
-
let parsedRuleData = [];
|
|
4150
|
-
|
|
4151
|
-
parseRulesBtn.addEventListener('click', async () => {
|
|
4152
|
-
const text = ruleInput.value.trim();
|
|
4153
|
-
if (!text) return;
|
|
4154
|
-
|
|
4155
|
-
parseRulesBtn.disabled = true;
|
|
4156
|
-
parseRulesBtn.textContent = 'Parsing...';
|
|
4157
|
-
ruleStatusEl.textContent = '';
|
|
4158
|
-
ruleStatusEl.className = 'rule-status';
|
|
4159
|
-
|
|
4160
|
-
try {
|
|
4161
|
-
const resp = await fetch('/api/parse-rules', {
|
|
4162
|
-
method: 'POST',
|
|
4163
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4164
|
-
body: JSON.stringify({ text, worldId: currentWorld ? currentWorld.id : 'social_simulation' }),
|
|
4165
|
-
});
|
|
4166
|
-
const data = await resp.json();
|
|
4167
|
-
|
|
4168
|
-
if (data.error) {
|
|
4169
|
-
ruleStatusEl.textContent = data.error;
|
|
4170
|
-
ruleStatusEl.className = 'rule-status error';
|
|
4171
|
-
parsedRulesEl.innerHTML = '';
|
|
4172
|
-
parsedRuleData = [];
|
|
4173
|
-
} else {
|
|
4174
|
-
parsedRuleData = data.rules || [];
|
|
4175
|
-
parsedRulesEl.innerHTML = parsedRuleData.map((r, i) => {
|
|
4176
|
-
const enfType = r.enforcement || 'block';
|
|
4177
|
-
const iconMap = { block: '🔴', allow: '🟢', modify: '🔵', warn: '🟡', pause: '🟡' };
|
|
4178
|
-
const labelMap = { block: 'Gate', allow: 'Invariant', modify: 'Modifier', warn: 'Warning', pause: 'Warning' };
|
|
4179
|
-
const effectMap = { block: 'Blocks actions', allow: 'Always enforced', modify: 'Adjusts behavior', warn: 'Signals risk', pause: 'Signals risk' };
|
|
4180
|
-
const icon = iconMap[enfType] || '🟢';
|
|
4181
|
-
const label = labelMap[enfType] || 'Rule';
|
|
4182
|
-
const effect = effectMap[enfType] || 'Active';
|
|
4183
|
-
return '<div class="parsed-rule enforcement-' + enfType + '">' +
|
|
4184
|
-
'<div class="pr-header"><span class="pr-icon">' + icon + '</span><span class="pr-action">' + label + '</span></div>' +
|
|
4185
|
-
'<div class="pr-desc">' + r.description + '</div>' +
|
|
4186
|
-
'<div class="pr-patterns">' + effect + ' • Matches: ' + r.intent_patterns.join(', ') + '</div>' +
|
|
4187
|
-
'</div>';
|
|
4188
|
-
}).join('');
|
|
4189
|
-
|
|
4190
|
-
if (parsedRuleData.length > 0) {
|
|
4191
|
-
const btnLabel = currentWorldSource === 'custom' ? 'Generate World with ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') : 'Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation';
|
|
4192
|
-
parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">' + btnLabel + '</button>';
|
|
4193
|
-
document.getElementById('apply-rules-btn').addEventListener('click', async () => {
|
|
4194
|
-
try {
|
|
4195
|
-
if (currentWorldSource === 'custom') {
|
|
4196
|
-
// CUSTOM MODE: Generate a full governed world (state vars, gates, thesis)
|
|
4197
|
-
const customName = document.getElementById('custom-world-name');
|
|
4198
|
-
const worldName = (customName && customName.value) ? customName.value : 'Custom World';
|
|
4199
|
-
const ruleText = document.getElementById('rule-input').value;
|
|
4200
|
-
|
|
4201
|
-
const genResp = await fetch('/api/generate-world', {
|
|
4202
|
-
method: 'POST',
|
|
4203
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4204
|
-
body: JSON.stringify({ text: ruleText, name: worldName }),
|
|
4205
|
-
});
|
|
4206
|
-
const genData = await genResp.json();
|
|
4207
|
-
|
|
4208
|
-
if (genData.status === 'generated') {
|
|
4209
|
-
ruleStatusEl.textContent = genData.parsed.total + ' rules parsed. Full world generated: ' + genData.world.stateVariables.length + ' state variables, ' + genData.world.gates.length + ' gates, ' + genData.world.invariants.length + ' invariants.';
|
|
4210
|
-
ruleStatusEl.className = 'rule-status success';
|
|
4211
|
-
|
|
4212
|
-
// Show invariants in active panel
|
|
4213
|
-
activeInvEl.innerHTML = genData.world.invariants.map(function(inv) {
|
|
4214
|
-
const color = inv.enforceable ? 'var(--red)' : 'var(--green)';
|
|
4215
|
-
return '<div class="inv-item" style="color:' + color + '">[' + inv.id + '] ' + inv.description + '</div>';
|
|
4216
|
-
}).join('');
|
|
4217
|
-
|
|
4218
|
-
// Set the generated world as the current world — with real state variables and gates
|
|
4219
|
-
currentWorld = {
|
|
4220
|
-
id: genData.world.id,
|
|
4221
|
-
title: genData.world.title,
|
|
4222
|
-
thesis: genData.world.thesis,
|
|
4223
|
-
stateVariables: genData.world.stateVariables,
|
|
4224
|
-
invariants: genData.world.invariants,
|
|
4225
|
-
gates: genData.world.gates,
|
|
4226
|
-
};
|
|
4227
|
-
worlds.push(currentWorld);
|
|
4228
|
-
|
|
4229
|
-
// Update world selector dropdown
|
|
4230
|
-
var opt = document.createElement('option');
|
|
4231
|
-
opt.value = currentWorld.id;
|
|
4232
|
-
opt.textContent = currentWorld.title;
|
|
4233
|
-
worldSelect.appendChild(opt);
|
|
4234
|
-
worldSelect.value = currentWorld.id;
|
|
4235
|
-
|
|
4236
|
-
// Render the state variable sliders
|
|
4237
|
-
selectWorld(currentWorld.id);
|
|
4238
|
-
document.getElementById('world-thesis').textContent = '"' + genData.world.thesis + '"';
|
|
4239
|
-
}
|
|
4240
|
-
} else {
|
|
4241
|
-
// PRESET MODE: Just apply rules on top of existing world
|
|
4242
|
-
let worldId = currentWorld ? currentWorld.id : 'social_simulation';
|
|
4243
|
-
|
|
4244
|
-
const applyResp = await fetch('/api/apply-rules', {
|
|
4245
|
-
method: 'POST',
|
|
4246
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4247
|
-
body: JSON.stringify({ rules: parsedRuleData, worldId }),
|
|
4248
|
-
});
|
|
4249
|
-
const applyData = await applyResp.json();
|
|
4250
|
-
if (applyData.status === 'applied') {
|
|
4251
|
-
ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
|
|
4252
|
-
ruleStatusEl.className = 'rule-status success';
|
|
4253
|
-
|
|
4254
|
-
// Show rules in active invariants panel
|
|
4255
|
-
activeInvEl.innerHTML = parsedRuleData.map(function(r) {
|
|
4256
|
-
var enfType = r.enforcement || 'block';
|
|
4257
|
-
var colorMap = { block: 'var(--red)', allow: 'var(--green)', modify: 'var(--blue)', warn: 'var(--yellow)', pause: 'var(--yellow)' };
|
|
4258
|
-
var color = colorMap[enfType] || 'var(--text-secondary)';
|
|
4259
|
-
return '<div class="inv-item" style="color:' + color + '">[' + r.id + '] ' + r.description + '</div>';
|
|
4260
|
-
}).join('');
|
|
4261
|
-
}
|
|
4262
|
-
}
|
|
4263
|
-
} catch (err) {
|
|
4264
|
-
ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
|
|
4265
|
-
ruleStatusEl.className = 'rule-status error';
|
|
4266
|
-
}
|
|
4267
|
-
});
|
|
4268
|
-
ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
|
|
4269
|
-
ruleStatusEl.className = 'rule-status success';
|
|
4270
|
-
}
|
|
4271
|
-
}
|
|
4272
|
-
} catch (err) {
|
|
4273
|
-
ruleStatusEl.textContent = 'Error: ' + err.message;
|
|
4274
|
-
ruleStatusEl.className = 'rule-status error';
|
|
4275
|
-
}
|
|
4276
|
-
|
|
4277
|
-
parseRulesBtn.disabled = false;
|
|
4278
|
-
parseRulesBtn.textContent = 'Parse Rules';
|
|
4279
|
-
});
|
|
4280
|
-
|
|
4281
5144
|
// ============================================
|
|
4282
5145
|
// SESSION TRACKING
|
|
4283
5146
|
// ============================================
|
|
@@ -4293,6 +5156,8 @@ async function pollSessionStats() {
|
|
|
4293
5156
|
if (el('s-blocked')) el('s-blocked').textContent = data.evaluations.blocked;
|
|
4294
5157
|
if (el('s-modified')) el('s-modified').textContent = data.evaluations.modified;
|
|
4295
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;
|
|
4296
5161
|
if (el('s-agents')) {
|
|
4297
5162
|
el('s-agents').textContent = data.agents.length > 0
|
|
4298
5163
|
? data.agents.length + ' agent(s): ' + data.agents.slice(0, 5).join(', ') + (data.agents.length > 5 ? '...' : '')
|
|
@@ -4354,6 +5219,189 @@ async function saveExperiment() {
|
|
|
4354
5219
|
sessionPollInterval = setInterval(pollSessionStats, 2000);
|
|
4355
5220
|
pollSessionStats();
|
|
4356
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
|
+
|
|
4357
5405
|
// ============================================
|
|
4358
5406
|
// BOOT
|
|
4359
5407
|
// ============================================
|