@neuroverseos/nv-sim 0.1.0

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.
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ /**
3
+ * POST /reason API Route Handler
4
+ *
5
+ * The HTTP interface for the Echelon reasoning engine.
6
+ * This is the "Stripe for reasoning" endpoint that agents and
7
+ * applications call for structured decision analysis.
8
+ *
9
+ * Endpoints:
10
+ * POST /api/v1/reason — Run structured reasoning on a scenario
11
+ * POST /api/v1/reason/capsule — Create a shareable scenario capsule
12
+ * GET /api/v1/reason/health — Engine health check
13
+ * GET /api/v1/reason/presets — List preset scenario templates
14
+ *
15
+ * Design:
16
+ * - Stateless: no data persisted after response
17
+ * - BYOK: caller provides their own API key (future)
18
+ * - Governance traces in every response (open governance layer)
19
+ * - Rate limiting per API key (future)
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.parseReasonRequestBody = parseReasonRequestBody;
23
+ exports.handleReasonRequest = handleReasonRequest;
24
+ exports.handleReasonFromCapsule = handleReasonFromCapsule;
25
+ exports.handleCreateCapsule = handleCreateCapsule;
26
+ exports.handleHealthCheck = handleHealthCheck;
27
+ exports.handleListPresets = handleListPresets;
28
+ exports.handleGetPreset = handleGetPreset;
29
+ exports.handleRunPreset = handleRunPreset;
30
+ const reasoningEngine_1 = require("./reasoningEngine");
31
+ const scenarioCapsule_1 = require("./scenarioCapsule");
32
+ // ============================================
33
+ // REQUEST PARSING
34
+ // ============================================
35
+ /**
36
+ * Parse and validate a POST /reason request body.
37
+ */
38
+ function parseReasonRequestBody(body) {
39
+ if (!body || typeof body !== "object") {
40
+ return {
41
+ error: {
42
+ status: "error",
43
+ error: {
44
+ code: "INVALID_REQUEST",
45
+ message: "Request body must be a JSON object",
46
+ },
47
+ reasoning_id: `rsn_${Date.now().toString(36)}_err`,
48
+ timestamp: new Date().toISOString(),
49
+ },
50
+ };
51
+ }
52
+ const b = body;
53
+ if (!b.scenario || typeof b.scenario !== "string") {
54
+ return {
55
+ error: {
56
+ status: "error",
57
+ error: {
58
+ code: "INVALID_REQUEST",
59
+ message: "\"scenario\" field is required and must be a string",
60
+ },
61
+ reasoning_id: `rsn_${Date.now().toString(36)}_err`,
62
+ timestamp: new Date().toISOString(),
63
+ },
64
+ };
65
+ }
66
+ // Build the request — the engine validates the rest
67
+ const request = {
68
+ scenario: b.scenario,
69
+ stakeholders: b.stakeholders,
70
+ assumptions: b.assumptions,
71
+ constraints: b.constraints,
72
+ depth: b.depth,
73
+ questions: b.questions,
74
+ perspective: b.perspective,
75
+ swarm: b.swarm,
76
+ };
77
+ return { request };
78
+ }
79
+ // ============================================
80
+ // ROUTE HANDLERS
81
+ // ============================================
82
+ /**
83
+ * POST /api/v1/reason
84
+ *
85
+ * Main reasoning endpoint. Accepts a scenario and returns
86
+ * structured analysis with governance traces.
87
+ *
88
+ * This is the endpoint that autonomous agents call.
89
+ */
90
+ async function handleReasonRequest(body) {
91
+ const parsed = parseReasonRequestBody(body);
92
+ if ("error" in parsed) {
93
+ return parsed.error;
94
+ }
95
+ return (0, reasoningEngine_1.processReasonRequest)(parsed.request);
96
+ }
97
+ /**
98
+ * POST /api/v1/reason (from capsule)
99
+ *
100
+ * Run reasoning from a scenario capsule.
101
+ * The capsule can come from a URL, a file, or another agent.
102
+ */
103
+ async function handleReasonFromCapsule(capsule) {
104
+ const request = (0, scenarioCapsule_1.capsuleToReasonRequest)(capsule);
105
+ return (0, reasoningEngine_1.processReasonRequest)(request);
106
+ }
107
+ function handleCreateCapsule(body) {
108
+ const capsule = (0, scenarioCapsule_1.createCapsule)(body.title, body.request, {
109
+ signals: body.signals,
110
+ tags: body.tags,
111
+ author: body.author,
112
+ });
113
+ return {
114
+ capsule,
115
+ encoded: (0, scenarioCapsule_1.encodeCapsule)(capsule),
116
+ shareable_url: (0, scenarioCapsule_1.buildShareableUrl)(capsule),
117
+ };
118
+ }
119
+ function handleHealthCheck() {
120
+ return {
121
+ status: "healthy",
122
+ engine_version: "0.1.0",
123
+ governance_layers: [
124
+ "echelon-constitutional (proprietary)",
125
+ "neuroverse-os-governance (open source)",
126
+ ],
127
+ capabilities: [
128
+ "multi-path reasoning",
129
+ "assumption challenging",
130
+ "outcome projection",
131
+ "swarm simulation",
132
+ "scenario capsules",
133
+ "governance tracing",
134
+ ],
135
+ };
136
+ }
137
+ function handleListPresets() {
138
+ return {
139
+ presets: Object.entries(scenarioCapsule_1.SCENARIO_TEMPLATES).map(([id, template]) => ({
140
+ id,
141
+ title: template.title,
142
+ tags: template.tags ?? [],
143
+ stakeholder_count: template.stakeholders.length,
144
+ })),
145
+ };
146
+ }
147
+ /**
148
+ * GET /api/v1/reason/presets/:id
149
+ *
150
+ * Get a specific preset scenario as a capsule, ready to run.
151
+ */
152
+ function handleGetPreset(presetId) {
153
+ try {
154
+ const capsule = (0, scenarioCapsule_1.getPresetCapsule)(presetId);
155
+ return {
156
+ capsule,
157
+ shareable_url: (0, scenarioCapsule_1.buildShareableUrl)(capsule),
158
+ };
159
+ }
160
+ catch {
161
+ return {
162
+ status: "error",
163
+ error: {
164
+ code: "INVALID_REQUEST",
165
+ message: `Unknown preset: "${presetId}". Use GET /api/v1/reason/presets for available presets.`,
166
+ },
167
+ reasoning_id: `rsn_${Date.now().toString(36)}_err`,
168
+ timestamp: new Date().toISOString(),
169
+ };
170
+ }
171
+ }
172
+ /**
173
+ * POST /api/v1/reason/preset/:id
174
+ *
175
+ * Run reasoning on a preset scenario.
176
+ * Convenience endpoint — loads the preset and runs it.
177
+ */
178
+ async function handleRunPreset(presetId, overrides) {
179
+ try {
180
+ const capsule = (0, scenarioCapsule_1.getPresetCapsule)(presetId);
181
+ const baseRequest = (0, scenarioCapsule_1.capsuleToReasonRequest)(capsule);
182
+ // Apply any overrides
183
+ const request = {
184
+ ...baseRequest,
185
+ ...overrides,
186
+ assumptions: {
187
+ ...baseRequest.assumptions,
188
+ ...overrides?.assumptions,
189
+ },
190
+ constraints: {
191
+ ...baseRequest.constraints,
192
+ ...overrides?.constraints,
193
+ },
194
+ };
195
+ return (0, reasoningEngine_1.processReasonRequest)(request);
196
+ }
197
+ catch {
198
+ return {
199
+ status: "error",
200
+ error: {
201
+ code: "INVALID_REQUEST",
202
+ message: `Unknown preset: "${presetId}"`,
203
+ },
204
+ reasoning_id: `rsn_${Date.now().toString(36)}_err`,
205
+ timestamp: new Date().toISOString(),
206
+ };
207
+ }
208
+ }
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ /**
3
+ * Chaos Engine — Automated Stress Testing for Governed Systems
4
+ *
5
+ * "Under what conditions does this system break?"
6
+ *
7
+ * Runs hundreds of randomized simulations to find:
8
+ * - System collapse probability (baseline vs governed)
9
+ * - Worst-case volatility
10
+ * - Most triggered governance rules
11
+ * - Breaking points and fragile assumptions
12
+ *
13
+ * Usage:
14
+ * npx nv-sim chaos # Stress test the trading demo
15
+ * npx nv-sim chaos --runs 500 # More iterations
16
+ * npx nv-sim chaos strait_of_hormuz # Test a preset
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.runChaosTest = runChaosTest;
20
+ const scenarioCapsule_1 = require("./scenarioCapsule");
21
+ const governedSimulation_1 = require("./governedSimulation");
22
+ // ============================================
23
+ // SCENARIO PERTURBATION
24
+ // ============================================
25
+ /** Simple seeded PRNG for reproducible chaos */
26
+ function createRng(seed) {
27
+ let s = seed;
28
+ return () => {
29
+ s = (s * 1664525 + 1013904223) & 0xffffffff;
30
+ return (s >>> 0) / 0xffffffff;
31
+ };
32
+ }
33
+ const DISPOSITIONS = ["hostile", "neutral", "supportive"];
34
+ function perturbStakeholders(stakeholders, rng, intensity) {
35
+ return stakeholders.map((s) => {
36
+ // Randomly shift dispositions based on intensity
37
+ if (rng() < intensity * 0.4) {
38
+ const newDisp = DISPOSITIONS[Math.floor(rng() * DISPOSITIONS.length)];
39
+ return { ...s, disposition: newDisp };
40
+ }
41
+ return { ...s };
42
+ });
43
+ }
44
+ function perturbScenario(scenario, rng) {
45
+ // Add random shock modifiers to the scenario text
46
+ const shocks = [
47
+ "A sudden liquidity crisis amplifies all pressures.",
48
+ "An unexpected regulatory announcement creates panic.",
49
+ "A major player unexpectedly exits the market.",
50
+ "Coordinated action by hostile actors intensifies pressure.",
51
+ "Communication systems experience partial failure.",
52
+ "Public sentiment shifts dramatically against incumbents.",
53
+ "A previously hidden risk factor suddenly materializes.",
54
+ "Cross-market contagion accelerates beyond models.",
55
+ "A trusted institution's credibility collapses.",
56
+ "External geopolitical shock compounds the crisis.",
57
+ ];
58
+ const numShocks = Math.floor(rng() * 3) + 1;
59
+ const selected = [];
60
+ for (let i = 0; i < numShocks; i++) {
61
+ const idx = Math.floor(rng() * shocks.length);
62
+ if (!selected.includes(shocks[idx])) {
63
+ selected.push(shocks[idx]);
64
+ }
65
+ }
66
+ return scenario + " " + selected.join(" ");
67
+ }
68
+ function perturbPaths(paths, rng, intensity) {
69
+ return paths.map((p) => ({
70
+ ...p,
71
+ probability: Math.max(0.05, Math.min(0.95, p.probability + (rng() - 0.5) * intensity * 0.4)),
72
+ risk: rng() < intensity * 0.3
73
+ ? ["moderate", "high", "critical"][Math.floor(rng() * 3)]
74
+ : p.risk,
75
+ }));
76
+ }
77
+ async function runChaosTest(presetId, options = {}) {
78
+ const { runs = 100, intensity = 0.6, seed = Date.now(), onProgress, } = options;
79
+ // Resolve scenario
80
+ const { request, world, paths } = resolvePreset(presetId);
81
+ const startTime = Date.now();
82
+ const rng = createRng(seed);
83
+ // Collect per-run results
84
+ const results = [];
85
+ for (let i = 0; i < runs; i++) {
86
+ // Perturb the scenario for this run
87
+ const perturbedRequest = {
88
+ ...request,
89
+ scenario: perturbScenario(request.scenario, rng),
90
+ stakeholders: perturbStakeholders(request.stakeholders, rng, intensity),
91
+ };
92
+ const perturbedPaths = perturbPaths(paths, rng, intensity);
93
+ const result = await (0, governedSimulation_1.runGovernedComparison)(perturbedRequest, world, perturbedPaths);
94
+ results.push(result);
95
+ if (onProgress)
96
+ onProgress(i + 1, runs);
97
+ }
98
+ const durationMs = Date.now() - startTime;
99
+ // Aggregate
100
+ return aggregateResults(presetId, results, durationMs);
101
+ }
102
+ function resolvePreset(presetId) {
103
+ if (presetId === "trading" || presetId === "flash_crash") {
104
+ return {
105
+ request: governedSimulation_1.TRADING_DEMO.scenario,
106
+ world: governedSimulation_1.TRADING_DEMO.world,
107
+ paths: governedSimulation_1.TRADING_DEMO.paths,
108
+ };
109
+ }
110
+ const template = scenarioCapsule_1.SCENARIO_TEMPLATES[presetId];
111
+ if (!template) {
112
+ throw new Error(`Unknown preset: ${presetId}. Available: trading, ${Object.keys(scenarioCapsule_1.SCENARIO_TEMPLATES).join(", ")}`);
113
+ }
114
+ if (!template.world?.inline_definition) {
115
+ throw new Error(`Preset "${presetId}" has no world definition for chaos testing.`);
116
+ }
117
+ const request = {
118
+ scenario: template.scenario,
119
+ stakeholders: template.stakeholders,
120
+ assumptions: template.assumptions,
121
+ constraints: template.constraints,
122
+ depth: template.depth,
123
+ swarm: template.swarm,
124
+ };
125
+ const defaultPaths = [
126
+ {
127
+ id: "path_primary",
128
+ label: "Primary Response",
129
+ description: "Most likely course of action",
130
+ projected_outcome: "Moderate disruption with cascading effects",
131
+ probability: 0.6,
132
+ risk: "high",
133
+ tradeoffs: ["Speed vs. thoroughness"],
134
+ benefits_stakeholders: template.stakeholders.filter(s => s.disposition === "supportive").map(s => s.id),
135
+ harms_stakeholders: template.stakeholders.filter(s => s.disposition === "hostile").map(s => s.id),
136
+ },
137
+ {
138
+ id: "path_defensive",
139
+ label: "Defensive Posture",
140
+ description: "Conservative approach prioritizing stability",
141
+ projected_outcome: "Reduced damage but slower recovery",
142
+ probability: 0.7,
143
+ risk: "moderate",
144
+ tradeoffs: ["Safety vs. opportunity cost"],
145
+ benefits_stakeholders: template.stakeholders.filter(s => s.disposition !== "hostile").map(s => s.id),
146
+ harms_stakeholders: [],
147
+ },
148
+ ];
149
+ return { request, world: template.world.inline_definition, paths: defaultPaths };
150
+ }
151
+ // ============================================
152
+ // RESULT AGGREGATION
153
+ // ============================================
154
+ function aggregateResults(scenarioId, results, durationMs) {
155
+ const n = results.length;
156
+ // Collapse threshold: collapse probability >= 0.5
157
+ const COLLAPSE_THRESHOLD = 0.5;
158
+ // Baseline aggregates
159
+ const baselineCollapses = results.filter(r => r.baseline.metrics.collapseProbability >= COLLAPSE_THRESHOLD).length;
160
+ const baselineTrajectories = {};
161
+ let baselineTotalStability = 0;
162
+ let baselineMinStability = 1;
163
+ let baselineTotalVolatility = 0;
164
+ let baselineMaxVolatility = 0;
165
+ let baselineTotalCoalitionRisks = 0;
166
+ for (const r of results) {
167
+ const m = r.baseline.metrics;
168
+ baselineTotalStability += m.stabilityScore;
169
+ baselineMinStability = Math.min(baselineMinStability, m.stabilityScore);
170
+ baselineTotalVolatility += m.maxVolatility;
171
+ baselineMaxVolatility = Math.max(baselineMaxVolatility, m.maxVolatility);
172
+ baselineTotalCoalitionRisks += m.coalitionRisks;
173
+ const traj = r.baseline.swarm.trajectory;
174
+ baselineTrajectories[traj] = (baselineTrajectories[traj] ?? 0) + 1;
175
+ }
176
+ // Governed aggregates
177
+ const governedCollapses = results.filter(r => r.governed.metrics.collapseProbability >= COLLAPSE_THRESHOLD).length;
178
+ const governedTrajectories = {};
179
+ let governedTotalStability = 0;
180
+ let governedMinStability = 1;
181
+ let governedTotalVolatility = 0;
182
+ let governedMaxVolatility = 0;
183
+ let governedTotalCoalitionRisks = 0;
184
+ for (const r of results) {
185
+ const m = r.governed.metrics;
186
+ governedTotalStability += m.stabilityScore;
187
+ governedMinStability = Math.min(governedMinStability, m.stabilityScore);
188
+ governedTotalVolatility += m.maxVolatility;
189
+ governedMaxVolatility = Math.max(governedMaxVolatility, m.maxVolatility);
190
+ governedTotalCoalitionRisks += m.coalitionRisks;
191
+ const traj = r.governed.swarm.trajectory;
192
+ governedTrajectories[traj] = (governedTrajectories[traj] ?? 0) + 1;
193
+ }
194
+ // Impact aggregates
195
+ let totalEffectiveness = 0;
196
+ let improvementCount = 0;
197
+ let totalStabilityImprovement = 0;
198
+ let totalVolatilityReduction = 0;
199
+ for (const r of results) {
200
+ totalEffectiveness += r.comparison.governanceEffectiveness;
201
+ totalStabilityImprovement += r.comparison.stabilityImprovement;
202
+ totalVolatilityReduction += r.comparison.volatilityReduction;
203
+ if (r.comparison.governanceEffectiveness > 0)
204
+ improvementCount++;
205
+ }
206
+ // Most triggered rules — count gate triggers across all runs
207
+ const gateTriggerMap = new Map();
208
+ for (const r of results) {
209
+ for (const gate of r.worldRules.gates) {
210
+ const existing = gateTriggerMap.get(gate.id) ?? { label: gate.label, count: 0, severity: gate.severity };
211
+ existing.count++;
212
+ gateTriggerMap.set(gate.id, existing);
213
+ }
214
+ // Count from governance stats
215
+ for (const guardId of r.governanceStats.triggeredGuards) {
216
+ const existing = gateTriggerMap.get(guardId) ?? { label: guardId, count: 0, severity: "guard" };
217
+ existing.count++;
218
+ gateTriggerMap.set(guardId, existing);
219
+ }
220
+ }
221
+ const topRules = [...gateTriggerMap.entries()]
222
+ .map(([id, data]) => ({ ruleId: id, label: data.label, triggerCount: data.count, severity: data.severity }))
223
+ .sort((a, b) => b.triggerCount - a.triggerCount)
224
+ .slice(0, 5);
225
+ // Worst cases — runs with highest baseline collapse + lowest governed stability
226
+ const worstCases = results
227
+ .map((r, i) => ({
228
+ runIndex: i,
229
+ baselineCollapse: r.baseline.metrics.collapseProbability,
230
+ governedCollapse: r.governed.metrics.collapseProbability,
231
+ baselineStability: r.baseline.metrics.stabilityScore,
232
+ governedStability: r.governed.metrics.stabilityScore,
233
+ volatility: r.baseline.metrics.maxVolatility,
234
+ }))
235
+ .sort((a, b) => b.baselineCollapse - a.baselineCollapse)
236
+ .slice(0, 3);
237
+ // Engine stats
238
+ let totalGuardEvals = 0;
239
+ const totalVerdicts = { allow: 0, block: 0, pause: 0 };
240
+ let totalRulesFired = 0;
241
+ let engineLoaded = false;
242
+ for (const r of results) {
243
+ const gs = r.governanceStats;
244
+ if (gs.engineLoaded)
245
+ engineLoaded = true;
246
+ totalGuardEvals += gs.totalEvaluations;
247
+ totalVerdicts.allow += gs.verdicts.allow;
248
+ totalVerdicts.block += gs.verdicts.block;
249
+ totalVerdicts.pause += gs.verdicts.pause;
250
+ totalRulesFired += gs.rulesFired;
251
+ }
252
+ return {
253
+ scenarioId,
254
+ runsCompleted: n,
255
+ durationMs,
256
+ baseline: {
257
+ collapseCount: baselineCollapses,
258
+ collapseProbability: baselineCollapses / n,
259
+ avgStability: baselineTotalStability / n,
260
+ minStability: baselineMinStability,
261
+ avgVolatility: baselineTotalVolatility / n,
262
+ maxVolatility: baselineMaxVolatility,
263
+ avgCoalitionRisks: baselineTotalCoalitionRisks / n,
264
+ trajectories: baselineTrajectories,
265
+ },
266
+ governed: {
267
+ collapseCount: governedCollapses,
268
+ collapseProbability: governedCollapses / n,
269
+ avgStability: governedTotalStability / n,
270
+ minStability: governedMinStability,
271
+ avgVolatility: governedTotalVolatility / n,
272
+ maxVolatility: governedMaxVolatility,
273
+ avgCoalitionRisks: governedTotalCoalitionRisks / n,
274
+ trajectories: governedTrajectories,
275
+ },
276
+ impact: {
277
+ collapseReduction: (baselineCollapses - governedCollapses) / n * 100,
278
+ avgStabilityImprovement: totalStabilityImprovement / n,
279
+ avgVolatilityReduction: totalVolatilityReduction / n,
280
+ avgEffectiveness: totalEffectiveness / n,
281
+ improvementRate: improvementCount / n,
282
+ },
283
+ topRules,
284
+ worstCases,
285
+ engineStats: {
286
+ engineLoaded,
287
+ totalGuardEvals,
288
+ totalVerdicts,
289
+ totalRulesFired,
290
+ },
291
+ };
292
+ }