@neuroverseos/nv-sim 0.1.2 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +376 -66
- package/dist/adapters/mirofish.js +461 -0
- package/dist/adapters/scienceclaw.js +750 -0
- package/dist/assets/index-CHmUN8s0.js +532 -0
- package/dist/assets/index-DWgMnB7I.css +1 -0
- package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
- package/dist/assets/reportEngine-BVdQ2_nW.js +1 -0
- package/dist/components/ConstraintsPanel.js +11 -0
- package/dist/components/StakeholderBuilder.js +32 -0
- package/dist/components/ui/badge.js +24 -0
- package/dist/components/ui/button.js +70 -0
- package/dist/components/ui/card.js +57 -0
- package/dist/components/ui/input.js +44 -0
- package/dist/components/ui/label.js +45 -0
- package/dist/components/ui/select.js +70 -0
- package/dist/engine/aiProvider.js +681 -0
- package/dist/engine/auditTrace.js +352 -0
- package/dist/engine/behavioralAnalysis.js +605 -0
- package/dist/engine/cli.js +1408 -299
- package/dist/engine/dynamicsGovernance.js +588 -0
- package/dist/engine/fullGovernedLoop.js +367 -0
- package/dist/engine/governance.js +8 -3
- package/dist/engine/governedSimulation.js +114 -17
- package/dist/engine/index.js +56 -1
- package/dist/engine/liveAdapter.js +342 -0
- package/dist/engine/liveVisualizer.js +3063 -0
- package/dist/engine/metrics/science.metrics.js +335 -0
- package/dist/engine/narrativeInjection.js +305 -0
- package/dist/engine/policyEnforcement.js +1611 -0
- package/dist/engine/policyEngine.js +799 -0
- package/dist/engine/primeRadiant.js +540 -0
- package/dist/engine/reasoningEngine.js +57 -3
- package/dist/engine/reportEngine.js +97 -0
- package/dist/engine/scenarioComparison.js +463 -0
- package/dist/engine/scenarioLibrary.js +231 -0
- package/dist/engine/swarmSimulation.js +54 -1
- package/dist/engine/worldComparison.js +358 -0
- package/dist/engine/worldStorage.js +232 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +23 -0
- package/dist/lib/reasoningEngine.js +290 -0
- package/dist/lib/simulationAdapter.js +686 -0
- package/dist/lib/swarmParser.js +291 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils.js +8 -0
- package/dist/placeholder.svg +1 -0
- package/dist/robots.txt +14 -0
- package/dist/runtime/govern.js +473 -0
- package/dist/runtime/index.js +75 -0
- package/dist/runtime/types.js +11 -0
- package/package.json +17 -12
- package/variants/.gitkeep +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Scenario Library — Named Multi-Event Stress Scenarios
|
|
4
|
+
*
|
|
5
|
+
* The layer that turns NV-SIM from a simulation engine into
|
|
6
|
+
* a scenario modeling platform.
|
|
7
|
+
*
|
|
8
|
+
* Instead of manually wiring events:
|
|
9
|
+
* nv-sim compare --inject tanker_explosion@3,sanctions@5
|
|
10
|
+
*
|
|
11
|
+
* Users run named scenarios:
|
|
12
|
+
* nv-sim scenario taiwan_crisis
|
|
13
|
+
*
|
|
14
|
+
* Each scenario bundles a world preset + a sequence of narrative events
|
|
15
|
+
* that model a realistic multi-phase crisis.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* nv-sim scenario taiwan_crisis
|
|
19
|
+
* nv-sim scenario bank_run
|
|
20
|
+
* nv-sim scenario oil_shock --compare
|
|
21
|
+
* nv-sim scenarios # List all
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.SCENARIO_LIBRARY = void 0;
|
|
25
|
+
exports.resolveScenarioEvents = resolveScenarioEvents;
|
|
26
|
+
exports.getScenariosByCategory = getScenariosByCategory;
|
|
27
|
+
const narrativeInjection_1 = require("./narrativeInjection");
|
|
28
|
+
// ============================================
|
|
29
|
+
// BUILT-IN SCENARIOS
|
|
30
|
+
// ============================================
|
|
31
|
+
exports.SCENARIO_LIBRARY = {
|
|
32
|
+
// --- Geopolitical ---
|
|
33
|
+
taiwan_crisis: {
|
|
34
|
+
id: "taiwan_crisis",
|
|
35
|
+
title: "Taiwan Strait Crisis",
|
|
36
|
+
description: "Military escalation near Taiwan triggers cascading economic and diplomatic shocks",
|
|
37
|
+
world: "strait_of_hormuz",
|
|
38
|
+
events: [
|
|
39
|
+
"taiwan_exercises@2",
|
|
40
|
+
"sanctions@4",
|
|
41
|
+
"flash_crash_rumor@5",
|
|
42
|
+
"rate_cut@7",
|
|
43
|
+
],
|
|
44
|
+
compareWorlds: ["trading"],
|
|
45
|
+
category: "geopolitical",
|
|
46
|
+
rounds: 8,
|
|
47
|
+
},
|
|
48
|
+
hormuz_blockade: {
|
|
49
|
+
id: "hormuz_blockade",
|
|
50
|
+
title: "Strait of Hormuz Blockade",
|
|
51
|
+
description: "Tanker attack escalates to full shipping disruption with energy market cascade",
|
|
52
|
+
world: "strait_of_hormuz",
|
|
53
|
+
events: [
|
|
54
|
+
"tanker_explosion@2",
|
|
55
|
+
"sanctions@4",
|
|
56
|
+
"oil_discovery@6",
|
|
57
|
+
],
|
|
58
|
+
compareWorlds: ["gas_price_spike"],
|
|
59
|
+
category: "geopolitical",
|
|
60
|
+
rounds: 7,
|
|
61
|
+
},
|
|
62
|
+
// --- Financial ---
|
|
63
|
+
bank_run: {
|
|
64
|
+
id: "bank_run",
|
|
65
|
+
title: "Banking Crisis — Contagion Cascade",
|
|
66
|
+
description: "Major bank insolvency triggers contagion, liquidity crisis, and emergency intervention",
|
|
67
|
+
world: "trading",
|
|
68
|
+
events: [
|
|
69
|
+
"bank_collapse@2",
|
|
70
|
+
"flash_crash_rumor@3",
|
|
71
|
+
"rate_cut@4",
|
|
72
|
+
"stimulus_package@6",
|
|
73
|
+
],
|
|
74
|
+
compareWorlds: ["gas_price_spike"],
|
|
75
|
+
category: "financial",
|
|
76
|
+
rounds: 7,
|
|
77
|
+
},
|
|
78
|
+
flash_cascade: {
|
|
79
|
+
id: "flash_cascade",
|
|
80
|
+
title: "Flash Crash — Multi-Phase Cascade",
|
|
81
|
+
description: "Algorithmic failure triggers chain reaction across interconnected markets",
|
|
82
|
+
world: "trading",
|
|
83
|
+
events: [
|
|
84
|
+
"flash_crash_rumor@1",
|
|
85
|
+
"bank_collapse@3",
|
|
86
|
+
"rate_cut@5",
|
|
87
|
+
],
|
|
88
|
+
category: "financial",
|
|
89
|
+
rounds: 6,
|
|
90
|
+
},
|
|
91
|
+
// --- Energy ---
|
|
92
|
+
oil_shock: {
|
|
93
|
+
id: "oil_shock",
|
|
94
|
+
title: "Oil Supply Shock",
|
|
95
|
+
description: "Tanker attack + sanctions create compound energy crisis",
|
|
96
|
+
world: "gas_price_spike",
|
|
97
|
+
events: [
|
|
98
|
+
"tanker_explosion@2",
|
|
99
|
+
"sanctions@4",
|
|
100
|
+
"grid_failure@5",
|
|
101
|
+
"oil_discovery@7",
|
|
102
|
+
],
|
|
103
|
+
compareWorlds: ["strait_of_hormuz"],
|
|
104
|
+
category: "energy",
|
|
105
|
+
rounds: 8,
|
|
106
|
+
},
|
|
107
|
+
energy_transition_shock: {
|
|
108
|
+
id: "energy_transition_shock",
|
|
109
|
+
title: "Energy Transition Disruption",
|
|
110
|
+
description: "Grid failure during rapid transition triggers regulatory and market cascade",
|
|
111
|
+
world: "gas_price_spike",
|
|
112
|
+
events: [
|
|
113
|
+
"grid_failure@2",
|
|
114
|
+
"regulation_shock@3",
|
|
115
|
+
"stimulus_package@5",
|
|
116
|
+
],
|
|
117
|
+
category: "energy",
|
|
118
|
+
rounds: 6,
|
|
119
|
+
},
|
|
120
|
+
// --- Political ---
|
|
121
|
+
election_shock: {
|
|
122
|
+
id: "election_shock",
|
|
123
|
+
title: "Election Crisis",
|
|
124
|
+
description: "Political shock cascades into market and regulatory disruption",
|
|
125
|
+
world: "trading",
|
|
126
|
+
events: [
|
|
127
|
+
"candidate_indicted@2",
|
|
128
|
+
"flash_crash_rumor@3",
|
|
129
|
+
"regulation_shock@5",
|
|
130
|
+
"stimulus_package@6",
|
|
131
|
+
],
|
|
132
|
+
category: "political",
|
|
133
|
+
rounds: 7,
|
|
134
|
+
},
|
|
135
|
+
ai_crackdown: {
|
|
136
|
+
id: "ai_crackdown",
|
|
137
|
+
title: "AI Regulatory Crackdown",
|
|
138
|
+
description: "Overnight AI regulation triggers market panic and sector realignment",
|
|
139
|
+
world: "ai_regulation_crisis",
|
|
140
|
+
events: [
|
|
141
|
+
"regulation_shock@2",
|
|
142
|
+
"flash_crash_rumor@3",
|
|
143
|
+
"diplomatic_breakthrough@5",
|
|
144
|
+
],
|
|
145
|
+
compareWorlds: ["trading"],
|
|
146
|
+
category: "political",
|
|
147
|
+
rounds: 6,
|
|
148
|
+
},
|
|
149
|
+
// --- Compound ---
|
|
150
|
+
perfect_storm: {
|
|
151
|
+
id: "perfect_storm",
|
|
152
|
+
title: "Perfect Storm — Multi-Domain Crisis",
|
|
153
|
+
description: "Geopolitical, financial, and energy shocks converge simultaneously",
|
|
154
|
+
world: "trading",
|
|
155
|
+
events: [
|
|
156
|
+
"tanker_explosion@1",
|
|
157
|
+
"bank_collapse@2",
|
|
158
|
+
"sanctions@3",
|
|
159
|
+
"grid_failure@4",
|
|
160
|
+
"rate_cut@5",
|
|
161
|
+
"ceasefire@7",
|
|
162
|
+
],
|
|
163
|
+
compareWorlds: ["strait_of_hormuz", "gas_price_spike"],
|
|
164
|
+
category: "compound",
|
|
165
|
+
rounds: 8,
|
|
166
|
+
},
|
|
167
|
+
black_swan: {
|
|
168
|
+
id: "black_swan",
|
|
169
|
+
title: "Black Swan Cascade",
|
|
170
|
+
description: "Extreme low-probability events in rapid succession",
|
|
171
|
+
world: "trading",
|
|
172
|
+
events: [
|
|
173
|
+
"bank_collapse@1",
|
|
174
|
+
"tanker_explosion@2",
|
|
175
|
+
"taiwan_exercises@3",
|
|
176
|
+
"grid_failure@4",
|
|
177
|
+
"regulation_shock@5",
|
|
178
|
+
],
|
|
179
|
+
compareWorlds: ["strait_of_hormuz"],
|
|
180
|
+
category: "compound",
|
|
181
|
+
rounds: 6,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
// ============================================
|
|
185
|
+
// SCENARIO RESOLUTION
|
|
186
|
+
// ============================================
|
|
187
|
+
/**
|
|
188
|
+
* Parse a scenario's event list into NarrativeEvent objects.
|
|
189
|
+
*/
|
|
190
|
+
function resolveScenarioEvents(scenario) {
|
|
191
|
+
const events = [];
|
|
192
|
+
for (const spec of scenario.events) {
|
|
193
|
+
const atIdx = spec.lastIndexOf("@");
|
|
194
|
+
if (atIdx === -1)
|
|
195
|
+
continue;
|
|
196
|
+
const eventId = spec.slice(0, atIdx).trim();
|
|
197
|
+
const round = parseInt(spec.slice(atIdx + 1), 10);
|
|
198
|
+
if (isNaN(round))
|
|
199
|
+
continue;
|
|
200
|
+
const preset = narrativeInjection_1.NARRATIVE_PRESETS[eventId];
|
|
201
|
+
if (preset) {
|
|
202
|
+
events.push({ ...preset, round });
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
events.push({
|
|
206
|
+
id: `custom_${round}`,
|
|
207
|
+
headline: eventId,
|
|
208
|
+
round,
|
|
209
|
+
severity: "major",
|
|
210
|
+
targets: [],
|
|
211
|
+
direction: "mixed",
|
|
212
|
+
propagation: "normal",
|
|
213
|
+
category: "custom",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return events;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get all scenarios grouped by category.
|
|
221
|
+
*/
|
|
222
|
+
function getScenariosByCategory() {
|
|
223
|
+
const grouped = {};
|
|
224
|
+
for (const scenario of Object.values(exports.SCENARIO_LIBRARY)) {
|
|
225
|
+
if (!grouped[scenario.category]) {
|
|
226
|
+
grouped[scenario.category] = [];
|
|
227
|
+
}
|
|
228
|
+
grouped[scenario.category].push(scenario);
|
|
229
|
+
}
|
|
230
|
+
return grouped;
|
|
231
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.runSwarmSimulation = runSwarmSimulation;
|
|
22
|
+
const aiProvider_1 = require("./aiProvider");
|
|
22
23
|
// ============================================
|
|
23
24
|
// REACTION MODELS
|
|
24
25
|
// ============================================
|
|
@@ -205,10 +206,16 @@ function findInflectionPoints(rounds) {
|
|
|
205
206
|
* This is Echelon's native reaction model — NOT MiroFish.
|
|
206
207
|
* It simulates how stakeholders react to different reasoning paths
|
|
207
208
|
* over multiple rounds, detecting emergent dynamics.
|
|
209
|
+
*
|
|
210
|
+
* When ANTHROPIC_API_KEY is set, agents use Claude for real reasoning.
|
|
211
|
+
* Otherwise, falls back to the deterministic Math.random() model.
|
|
208
212
|
*/
|
|
209
213
|
async function runSwarmSimulation(scenario, stakeholders, paths, config) {
|
|
210
214
|
const roundCount = config.rounds ?? 3;
|
|
211
215
|
const model = config.reaction_model ?? "mixed";
|
|
216
|
+
// Detect if AI reasoning is available and requested
|
|
217
|
+
const defaultProvider = (0, aiProvider_1.getDefaultProviderName)();
|
|
218
|
+
const useAI = config.ai_reasoning !== false && defaultProvider !== "deterministic";
|
|
212
219
|
// Filter stakeholders if specific ones requested
|
|
213
220
|
const activeStakeholders = config.simulate_stakeholders
|
|
214
221
|
? stakeholders.filter((s) => config.simulate_stakeholders.includes(s.id))
|
|
@@ -224,8 +231,54 @@ async function runSwarmSimulation(scenario, stakeholders, paths, config) {
|
|
|
224
231
|
}
|
|
225
232
|
// Run simulation rounds
|
|
226
233
|
const rounds = [];
|
|
234
|
+
let aiProvider;
|
|
235
|
+
if (useAI) {
|
|
236
|
+
try {
|
|
237
|
+
aiProvider = (0, aiProvider_1.getAIProvider)(defaultProvider);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// No API key or provider unavailable — fall back silently
|
|
241
|
+
}
|
|
242
|
+
}
|
|
227
243
|
for (let round = 0; round < roundCount; round++) {
|
|
228
|
-
|
|
244
|
+
let reactions;
|
|
245
|
+
if (aiProvider) {
|
|
246
|
+
// AI-POWERED REASONING: Each agent gets a Claude call
|
|
247
|
+
const previousReactions = round > 0
|
|
248
|
+
? rounds[round - 1].reactions.map(r => `${r.stakeholder_id}: ${r.reaction}`)
|
|
249
|
+
: [];
|
|
250
|
+
reactions = await Promise.all(activeStakeholders.map(async (stakeholder) => {
|
|
251
|
+
try {
|
|
252
|
+
const aiResult = await (0, aiProvider_1.generateAIReaction)({
|
|
253
|
+
stakeholderId: stakeholder.id,
|
|
254
|
+
stakeholderDescription: stakeholder.description,
|
|
255
|
+
stakeholderDisposition: stakeholder.disposition,
|
|
256
|
+
stakeholderPriorities: stakeholder.priorities,
|
|
257
|
+
scenario,
|
|
258
|
+
pathDescription: primaryPath.description,
|
|
259
|
+
pathRisk: primaryPath.risk,
|
|
260
|
+
round,
|
|
261
|
+
previousReactions,
|
|
262
|
+
provider: aiProvider,
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
stakeholder_id: stakeholder.id,
|
|
266
|
+
reaction: aiResult.reaction,
|
|
267
|
+
confidence: aiResult.confidence,
|
|
268
|
+
impact: aiResult.impact,
|
|
269
|
+
trigger: aiResult.trigger,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Individual agent fallback to deterministic
|
|
274
|
+
return modelReaction(stakeholder, primaryPath, model, round);
|
|
275
|
+
}
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// DETERMINISTIC: Original Math.random() model
|
|
280
|
+
reactions = activeStakeholders.map((stakeholder) => modelReaction(stakeholder, primaryPath, model, round));
|
|
281
|
+
}
|
|
229
282
|
const emergentDynamics = detectEmergentDynamics(reactions);
|
|
230
283
|
rounds.push({
|
|
231
284
|
round,
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* World Comparison Engine — Same Swarm, Different Rule Environments
|
|
4
|
+
*
|
|
5
|
+
* "NV-SIM lets you run the same swarm through different rule environments
|
|
6
|
+
* to see how governance changes emergent behavior."
|
|
7
|
+
*
|
|
8
|
+
* Unlike `compare` (baseline vs governed), this runs TWO governed simulations
|
|
9
|
+
* with different world rules. Same agents, same scenario — only the rules change.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* nv-sim worlds strait_of_hormuz gas_price_spike
|
|
13
|
+
* nv-sim worlds trading strait_of_hormuz
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.resolveWorld = resolveWorld;
|
|
17
|
+
exports.runWorldComparison = runWorldComparison;
|
|
18
|
+
exports.getAvailableWorlds = getAvailableWorlds;
|
|
19
|
+
exports.explainWorldGovernance = explainWorldGovernance;
|
|
20
|
+
exports.generateComparisonImpact = generateComparisonImpact;
|
|
21
|
+
const scenarioCapsule_1 = require("./scenarioCapsule");
|
|
22
|
+
const governedSimulation_1 = require("./governedSimulation");
|
|
23
|
+
const worldStorage_1 = require("./worldStorage");
|
|
24
|
+
function resolveWorld(presetId) {
|
|
25
|
+
// Check saved worlds first (browser localStorage)
|
|
26
|
+
const saved = resolveSavedWorld(presetId);
|
|
27
|
+
if (saved)
|
|
28
|
+
return saved;
|
|
29
|
+
if (presetId === "trading" || presetId === "flash_crash") {
|
|
30
|
+
return {
|
|
31
|
+
id: "trading-flash-crash",
|
|
32
|
+
title: "Flash Crash — Algorithmic Cascade",
|
|
33
|
+
scenario: governedSimulation_1.TRADING_DEMO.scenario.scenario,
|
|
34
|
+
stakeholders: governedSimulation_1.TRADING_DEMO.scenario.stakeholders,
|
|
35
|
+
assumptions: governedSimulation_1.TRADING_DEMO.scenario.assumptions,
|
|
36
|
+
constraints: governedSimulation_1.TRADING_DEMO.scenario.constraints,
|
|
37
|
+
swarm: governedSimulation_1.TRADING_DEMO.scenario.swarm,
|
|
38
|
+
depth: governedSimulation_1.TRADING_DEMO.scenario.depth,
|
|
39
|
+
world: governedSimulation_1.TRADING_DEMO.world,
|
|
40
|
+
paths: governedSimulation_1.TRADING_DEMO.paths,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const template = scenarioCapsule_1.SCENARIO_TEMPLATES[presetId];
|
|
44
|
+
if (!template) {
|
|
45
|
+
throw new Error(`Unknown preset: ${presetId}. Available: trading, ${Object.keys(scenarioCapsule_1.SCENARIO_TEMPLATES).join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
if (!template.world?.inline_definition) {
|
|
48
|
+
throw new Error(`Preset "${presetId}" has no world definition.`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
id: template.world.world_id,
|
|
52
|
+
title: template.title,
|
|
53
|
+
scenario: template.scenario,
|
|
54
|
+
stakeholders: template.stakeholders,
|
|
55
|
+
assumptions: template.assumptions,
|
|
56
|
+
constraints: template.constraints,
|
|
57
|
+
swarm: (template.swarm ?? { enabled: true, rounds: 5, reaction_model: "mixed" }),
|
|
58
|
+
depth: template.depth,
|
|
59
|
+
world: template.world.inline_definition,
|
|
60
|
+
paths: [
|
|
61
|
+
{
|
|
62
|
+
id: "path_primary",
|
|
63
|
+
label: "Primary Response",
|
|
64
|
+
description: "Most likely course of action",
|
|
65
|
+
projected_outcome: "Moderate disruption with cascading effects",
|
|
66
|
+
probability: 0.6,
|
|
67
|
+
risk: "high",
|
|
68
|
+
tradeoffs: ["Speed vs. thoroughness", "Short-term vs. long-term"],
|
|
69
|
+
benefits_stakeholders: template.stakeholders.filter(s => s.disposition === "supportive").map(s => s.id),
|
|
70
|
+
harms_stakeholders: template.stakeholders.filter(s => s.disposition === "hostile").map(s => s.id),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "path_defensive",
|
|
74
|
+
label: "Defensive Posture",
|
|
75
|
+
description: "Conservative approach prioritizing stability",
|
|
76
|
+
projected_outcome: "Reduced damage but slower recovery",
|
|
77
|
+
probability: 0.7,
|
|
78
|
+
risk: "moderate",
|
|
79
|
+
tradeoffs: ["Safety vs. opportunity cost"],
|
|
80
|
+
benefits_stakeholders: template.stakeholders.filter(s => s.disposition !== "hostile").map(s => s.id),
|
|
81
|
+
harms_stakeholders: [],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a saved world (from localStorage) into a ResolvedWorld.
|
|
88
|
+
* Saved worlds get merged with the base preset's scenario/stakeholders
|
|
89
|
+
* so they work in the comparison engine.
|
|
90
|
+
*/
|
|
91
|
+
function resolveSavedWorld(id) {
|
|
92
|
+
try {
|
|
93
|
+
const saved = (0, worldStorage_1.loadWorldFromStorage)(id);
|
|
94
|
+
if (!saved)
|
|
95
|
+
return null;
|
|
96
|
+
// Try to get scenario context from the base preset
|
|
97
|
+
let baseScenario = `Simulation with custom world: ${saved.world.thesis}`;
|
|
98
|
+
let baseStakeholders = [
|
|
99
|
+
{ id: "System", disposition: "neutral", priorities: ["stability"] },
|
|
100
|
+
{ id: "Agents", disposition: "unknown", priorities: ["execution"] },
|
|
101
|
+
];
|
|
102
|
+
let baseAssumptions = { severity: "high" };
|
|
103
|
+
let baseConstraints = {};
|
|
104
|
+
try {
|
|
105
|
+
const base = resolveWorld(saved.basePreset);
|
|
106
|
+
baseScenario = base.scenario;
|
|
107
|
+
baseStakeholders = base.stakeholders;
|
|
108
|
+
baseAssumptions = base.assumptions;
|
|
109
|
+
baseConstraints = base.constraints;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Base preset not found — use defaults
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
id: saved.id,
|
|
116
|
+
title: saved.name,
|
|
117
|
+
scenario: baseScenario,
|
|
118
|
+
stakeholders: baseStakeholders,
|
|
119
|
+
assumptions: baseAssumptions,
|
|
120
|
+
constraints: baseConstraints,
|
|
121
|
+
swarm: { enabled: true, rounds: 5, reaction_model: "mixed" },
|
|
122
|
+
depth: "full",
|
|
123
|
+
world: saved.world,
|
|
124
|
+
paths: [
|
|
125
|
+
{
|
|
126
|
+
id: "path_primary",
|
|
127
|
+
label: "Primary Response",
|
|
128
|
+
description: "Most likely course of action",
|
|
129
|
+
projected_outcome: "Moderate disruption with cascading effects",
|
|
130
|
+
probability: 0.6,
|
|
131
|
+
risk: "high",
|
|
132
|
+
tradeoffs: ["Speed vs. thoroughness", "Short-term vs. long-term"],
|
|
133
|
+
benefits_stakeholders: baseStakeholders.filter(s => s.disposition === "supportive").map(s => s.id),
|
|
134
|
+
harms_stakeholders: baseStakeholders.filter(s => s.disposition === "hostile").map(s => s.id),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function runWorldComparison(worldAId, worldBId, options) {
|
|
144
|
+
const worldADef = resolveWorld(worldAId);
|
|
145
|
+
const worldBDef = resolveWorld(worldBId);
|
|
146
|
+
// Use the scenario from world A as the shared scenario,
|
|
147
|
+
// but merge stakeholders from both worlds for richer simulation
|
|
148
|
+
const sharedScenario = worldADef.scenario + " " + worldBDef.scenario;
|
|
149
|
+
const mergedStakeholders = mergeStakeholders(worldADef.stakeholders, worldBDef.stakeholders);
|
|
150
|
+
// Build paths that work for both worlds
|
|
151
|
+
const sharedPaths = worldADef.paths;
|
|
152
|
+
options?.onProgress?.("World A", "simulating");
|
|
153
|
+
// Run World A governed comparison
|
|
154
|
+
const resultA = await (0, governedSimulation_1.runGovernedComparison)({
|
|
155
|
+
scenario: sharedScenario,
|
|
156
|
+
stakeholders: mergedStakeholders,
|
|
157
|
+
assumptions: worldADef.assumptions,
|
|
158
|
+
constraints: worldADef.constraints,
|
|
159
|
+
depth: worldADef.depth,
|
|
160
|
+
swarm: worldADef.swarm,
|
|
161
|
+
}, worldADef.world, sharedPaths);
|
|
162
|
+
options?.onProgress?.("World B", "simulating");
|
|
163
|
+
// Run World B governed comparison
|
|
164
|
+
const resultB = await (0, governedSimulation_1.runGovernedComparison)({
|
|
165
|
+
scenario: sharedScenario,
|
|
166
|
+
stakeholders: mergedStakeholders,
|
|
167
|
+
assumptions: worldBDef.assumptions,
|
|
168
|
+
constraints: worldBDef.constraints,
|
|
169
|
+
depth: worldBDef.depth,
|
|
170
|
+
swarm: worldBDef.swarm,
|
|
171
|
+
}, worldBDef.world, sharedPaths);
|
|
172
|
+
// Extract round snapshots
|
|
173
|
+
const roundsA = extractRoundSnapshots(resultA);
|
|
174
|
+
const roundsB = extractRoundSnapshots(resultB);
|
|
175
|
+
// Build delta
|
|
176
|
+
const metricsA = resultA.governed.metrics;
|
|
177
|
+
const metricsB = resultB.governed.metrics;
|
|
178
|
+
const stabilityDiff = metricsA.stabilityScore - metricsB.stabilityScore;
|
|
179
|
+
const volatilityDiff = metricsA.maxVolatility - metricsB.maxVolatility;
|
|
180
|
+
const coalitionRiskDiff = metricsA.coalitionRisks - metricsB.coalitionRisks;
|
|
181
|
+
const collapseDiff = metricsA.collapseProbability - metricsB.collapseProbability;
|
|
182
|
+
const moreStable = Math.abs(stabilityDiff) < 0.02 ? "equal"
|
|
183
|
+
: stabilityDiff > 0 ? "A" : "B";
|
|
184
|
+
const narrative = buildWorldNarrative(worldADef, worldBDef, metricsA, metricsB, moreStable);
|
|
185
|
+
return {
|
|
186
|
+
scenario: sharedScenario,
|
|
187
|
+
worldA: {
|
|
188
|
+
id: worldAId,
|
|
189
|
+
thesis: worldADef.world.thesis,
|
|
190
|
+
invariantCount: worldADef.world.invariants.length,
|
|
191
|
+
gateCount: (worldADef.world.gates ?? []).length,
|
|
192
|
+
metrics: metricsA,
|
|
193
|
+
trajectory: resultA.governed.swarm.trajectory,
|
|
194
|
+
governanceStats: resultA.governanceStats,
|
|
195
|
+
rounds: roundsA,
|
|
196
|
+
},
|
|
197
|
+
worldB: {
|
|
198
|
+
id: worldBId,
|
|
199
|
+
thesis: worldBDef.world.thesis,
|
|
200
|
+
invariantCount: worldBDef.world.invariants.length,
|
|
201
|
+
gateCount: (worldBDef.world.gates ?? []).length,
|
|
202
|
+
metrics: metricsB,
|
|
203
|
+
trajectory: resultB.governed.swarm.trajectory,
|
|
204
|
+
governanceStats: resultB.governanceStats,
|
|
205
|
+
rounds: roundsB,
|
|
206
|
+
},
|
|
207
|
+
delta: {
|
|
208
|
+
stabilityDiff: Number((stabilityDiff * 100).toFixed(1)),
|
|
209
|
+
volatilityDiff: Number((volatilityDiff * 100).toFixed(1)),
|
|
210
|
+
coalitionRiskDiff,
|
|
211
|
+
collapseDiff: Number((collapseDiff * 100).toFixed(1)),
|
|
212
|
+
moreStable,
|
|
213
|
+
narrative,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function mergeStakeholders(a, b) {
|
|
218
|
+
const seen = new Set();
|
|
219
|
+
const merged = [];
|
|
220
|
+
for (const s of [...a, ...b]) {
|
|
221
|
+
if (!seen.has(s.id)) {
|
|
222
|
+
seen.add(s.id);
|
|
223
|
+
merged.push(s);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return merged;
|
|
227
|
+
}
|
|
228
|
+
function extractRoundSnapshots(result) {
|
|
229
|
+
return result.governed.swarm.rounds.map((r) => ({
|
|
230
|
+
round: r.round,
|
|
231
|
+
avgImpact: r.reactions.reduce((s, rx) => s + rx.impact, 0) / r.reactions.length,
|
|
232
|
+
maxVolatility: Math.max(...r.reactions.map((rx) => Math.abs(rx.impact))),
|
|
233
|
+
agentImpacts: r.reactions.map((rx) => ({
|
|
234
|
+
id: rx.stakeholder_id,
|
|
235
|
+
impact: rx.impact,
|
|
236
|
+
confidence: rx.confidence,
|
|
237
|
+
})),
|
|
238
|
+
interventions: r.emergent_dynamics ?? [],
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
function buildWorldNarrative(worldA, worldB, metricsA, metricsB, moreStable) {
|
|
242
|
+
const parts = [];
|
|
243
|
+
parts.push(`World A ("${worldA.world.thesis.slice(0, 60)}...") vs World B ("${worldB.world.thesis.slice(0, 60)}...").`);
|
|
244
|
+
if (moreStable === "A") {
|
|
245
|
+
parts.push(`World A produced a more stable system (${(metricsA.stabilityScore * 100).toFixed(0)}% vs ${(metricsB.stabilityScore * 100).toFixed(0)}%).`);
|
|
246
|
+
}
|
|
247
|
+
else if (moreStable === "B") {
|
|
248
|
+
parts.push(`World B produced a more stable system (${(metricsB.stabilityScore * 100).toFixed(0)}% vs ${(metricsA.stabilityScore * 100).toFixed(0)}%).`);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
parts.push(`Both worlds produced similar stability levels.`);
|
|
252
|
+
}
|
|
253
|
+
const invDiff = worldA.world.invariants.length - worldB.world.invariants.length;
|
|
254
|
+
if (invDiff !== 0) {
|
|
255
|
+
parts.push(`World ${invDiff > 0 ? "A" : "B"} has ${Math.abs(invDiff)} more invariant${Math.abs(invDiff) > 1 ? "s" : ""}, providing tighter structural constraints.`);
|
|
256
|
+
}
|
|
257
|
+
parts.push("Same agents, different rules — the world shapes the outcome.");
|
|
258
|
+
return parts.join(" ");
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get all available world presets for the comparison picker.
|
|
262
|
+
*/
|
|
263
|
+
function getAvailableWorlds() {
|
|
264
|
+
const worlds = [
|
|
265
|
+
{
|
|
266
|
+
id: "trading",
|
|
267
|
+
title: "Flash Crash — Algorithmic Cascade",
|
|
268
|
+
thesis: governedSimulation_1.TRADING_DEMO.world.thesis,
|
|
269
|
+
invariantCount: governedSimulation_1.TRADING_DEMO.world.invariants.length,
|
|
270
|
+
gateCount: (governedSimulation_1.TRADING_DEMO.world.gates ?? []).length,
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
for (const [key, template] of Object.entries(scenarioCapsule_1.SCENARIO_TEMPLATES)) {
|
|
274
|
+
if (template.world?.inline_definition) {
|
|
275
|
+
worlds.push({
|
|
276
|
+
id: key,
|
|
277
|
+
title: template.title,
|
|
278
|
+
thesis: template.world.inline_definition.thesis,
|
|
279
|
+
invariantCount: template.world.inline_definition.invariants.length,
|
|
280
|
+
gateCount: (template.world.inline_definition.gates ?? []).length,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return worlds;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Generate a plain-English explanation of a world's governance posture.
|
|
288
|
+
* Uses the governance engine's own data — no LLM needed.
|
|
289
|
+
*/
|
|
290
|
+
function explainWorldGovernance(presetId) {
|
|
291
|
+
const world = resolveWorld(presetId);
|
|
292
|
+
const invariants = world.world.invariants;
|
|
293
|
+
const gates = world.world.gates ?? [];
|
|
294
|
+
const enforceableCount = invariants.filter(i => i.enforceable).length;
|
|
295
|
+
const advisoryCount = invariants.length - enforceableCount;
|
|
296
|
+
const invariantSummary = invariants.length === 0
|
|
297
|
+
? "No invariants defined — ungoverned."
|
|
298
|
+
: `${invariants.length} rules (${enforceableCount} enforced, ${advisoryCount} advisory): ${invariants.map(i => i.description).join("; ")}`;
|
|
299
|
+
const criticalGates = gates.filter(g => g.severity === "critical");
|
|
300
|
+
const warningGates = gates.filter(g => g.severity === "warning");
|
|
301
|
+
const gateSummary = gates.length === 0
|
|
302
|
+
? "No viability gates — no collapse detection."
|
|
303
|
+
: `${gates.length} gates (${criticalGates.length} critical, ${warningGates.length} warning): ${gates.map(g => `${g.label} [${g.severity}]`).join(", ")}`;
|
|
304
|
+
const riskProfile = criticalGates.length >= 2 ? "high"
|
|
305
|
+
: criticalGates.length >= 1 || warningGates.length >= 2 ? "moderate"
|
|
306
|
+
: "low";
|
|
307
|
+
return {
|
|
308
|
+
worldId: presetId,
|
|
309
|
+
title: world.title,
|
|
310
|
+
thesis: world.world.thesis,
|
|
311
|
+
invariantSummary,
|
|
312
|
+
gateSummary,
|
|
313
|
+
riskProfile,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Generate a counterfactual impact summary from comparison results.
|
|
318
|
+
* "What would have happened without these rules?"
|
|
319
|
+
*
|
|
320
|
+
* Powered by @neuroverseos/governance engine data.
|
|
321
|
+
*/
|
|
322
|
+
function generateComparisonImpact(result) {
|
|
323
|
+
const { worldA, worldB, delta } = result;
|
|
324
|
+
const winner = delta.moreStable;
|
|
325
|
+
const winnerSide = winner === "A" ? worldA : winner === "B" ? worldB : worldA;
|
|
326
|
+
const loserSide = winner === "A" ? worldB : winner === "B" ? worldA : worldB;
|
|
327
|
+
const interventionDiff = winnerSide.governanceStats.totalEvaluations - loserSide.governanceStats.totalEvaluations;
|
|
328
|
+
const blockDiff = winnerSide.governanceStats.verdicts.block - loserSide.governanceStats.verdicts.block;
|
|
329
|
+
const keyFindings = [];
|
|
330
|
+
if (Math.abs(delta.stabilityDiff) >= 5) {
|
|
331
|
+
keyFindings.push(`${Math.abs(delta.stabilityDiff).toFixed(0)}% stability ${delta.stabilityDiff > 0 ? "advantage for World A" : "advantage for World B"}`);
|
|
332
|
+
}
|
|
333
|
+
if (Math.abs(delta.collapseDiff) >= 5) {
|
|
334
|
+
keyFindings.push(`${Math.abs(delta.collapseDiff).toFixed(0)}% ${delta.collapseDiff > 0 ? "higher" : "lower"} cascade risk in World A`);
|
|
335
|
+
}
|
|
336
|
+
if (blockDiff !== 0) {
|
|
337
|
+
keyFindings.push(`${Math.abs(blockDiff)} more governance interventions in ${blockDiff > 0 ? "World A" : "World B"}`);
|
|
338
|
+
}
|
|
339
|
+
if (Math.abs(delta.volatilityDiff) >= 5) {
|
|
340
|
+
keyFindings.push(`${Math.abs(delta.volatilityDiff).toFixed(0)}% volatility ${delta.volatilityDiff > 0 ? "higher in A" : "higher in B"}`);
|
|
341
|
+
}
|
|
342
|
+
if (keyFindings.length === 0) {
|
|
343
|
+
keyFindings.push("Both worlds produced similar outcomes — rules had minimal differential effect.");
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
winner,
|
|
347
|
+
stabilityDelta: `${delta.stabilityDiff > 0 ? "+" : ""}${delta.stabilityDiff.toFixed(1)}%`,
|
|
348
|
+
cascadeRiskDelta: `${delta.collapseDiff > 0 ? "+" : ""}${delta.collapseDiff.toFixed(1)}%`,
|
|
349
|
+
volatilityDelta: `${delta.volatilityDiff > 0 ? "+" : ""}${delta.volatilityDiff.toFixed(1)}%`,
|
|
350
|
+
totalInterventionsA: worldA.governanceStats.totalEvaluations,
|
|
351
|
+
totalInterventionsB: worldB.governanceStats.totalEvaluations,
|
|
352
|
+
blockedActionsA: worldA.governanceStats.verdicts.block,
|
|
353
|
+
blockedActionsB: worldB.governanceStats.verdicts.block,
|
|
354
|
+
keyFindings,
|
|
355
|
+
verdict: delta.narrative,
|
|
356
|
+
poweredBy: "@neuroverseos/governance — simulateWorld() + evaluateGuard()",
|
|
357
|
+
};
|
|
358
|
+
}
|