@neuroverseos/nv-sim 0.1.0 → 0.1.4

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,305 @@
1
+ "use strict";
2
+ /**
3
+ * Narrative Injection — Information Shocks for Agent Simulations
4
+ *
5
+ * The third knob: agents + world rules + narrative events.
6
+ *
7
+ * Narrative injection models how information shocks propagate through
8
+ * agent networks. Instead of just changing rules, you inject events
9
+ * that shift beliefs, trigger reactions, and reorganize clusters.
10
+ *
11
+ * Usage:
12
+ * nv-sim compare --inject tanker_explosion@5
13
+ * nv-sim worlds trading strait_of_hormuz --inject rate_cut@3,sanctions@6
14
+ * nv-sim visualize --inject "China begins military exercises"@4
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.NARRATIVE_PRESETS = void 0;
18
+ exports.injectNarrative = injectNarrative;
19
+ exports.parseInjectArgs = parseInjectArgs;
20
+ exports.getEventsForRound = getEventsForRound;
21
+ // ============================================
22
+ // SEVERITY → MAGNITUDE MAPPING
23
+ // ============================================
24
+ const SEVERITY_MAGNITUDE = {
25
+ minor: 0.1,
26
+ moderate: 0.25,
27
+ major: 0.45,
28
+ extreme: 0.7,
29
+ };
30
+ const PROPAGATION_MULTIPLIER = {
31
+ slow: 0.6,
32
+ normal: 1.0,
33
+ viral: 1.4,
34
+ };
35
+ // ============================================
36
+ // NARRATIVE INJECTION ENGINE
37
+ // ============================================
38
+ /**
39
+ * Apply a narrative event to a set of agent reactions.
40
+ *
41
+ * This models how an information shock propagates through the network:
42
+ * - Targeted agents get the full impact
43
+ * - Related agents get partial impact (network effect)
44
+ * - Unrelated agents get minimal impact (background noise)
45
+ * - Confidence shifts based on event clarity
46
+ */
47
+ function injectNarrative(reactions, event, stakeholders) {
48
+ const magnitude = SEVERITY_MAGNITUDE[event.severity];
49
+ const propagation = PROPAGATION_MULTIPLIER[event.propagation];
50
+ const direction = event.direction === "positive" ? 1 : event.direction === "negative" ? -1 : 0;
51
+ const agentShifts = [];
52
+ const systemEffects = [];
53
+ systemEffects.push(`EVENT INJECTED: "${event.headline}"`);
54
+ systemEffects.push(`Severity: ${event.severity} | Propagation: ${event.propagation}`);
55
+ for (const reaction of reactions) {
56
+ const stakeholder = stakeholders.find(s => s.id === reaction.stakeholder_id);
57
+ const isTargeted = event.targets.some(t => reaction.stakeholder_id.toLowerCase().includes(t.toLowerCase()) ||
58
+ t.toLowerCase().includes(reaction.stakeholder_id.toLowerCase()));
59
+ // Determine how much this agent is affected
60
+ let sensitivity;
61
+ let reason;
62
+ if (isTargeted) {
63
+ // Directly targeted — full impact
64
+ sensitivity = 1.0;
65
+ reason = `directly targeted by "${event.headline}"`;
66
+ }
67
+ else if (stakeholder?.disposition === "hostile") {
68
+ // Hostile agents are more reactive to shocks
69
+ sensitivity = 0.6;
70
+ reason = `hostile disposition amplifies reaction to "${event.headline}"`;
71
+ }
72
+ else if (stakeholder?.disposition === "neutral") {
73
+ // Neutral agents react moderately
74
+ sensitivity = 0.35;
75
+ reason = `neutral stance — moderate reaction to "${event.headline}"`;
76
+ }
77
+ else {
78
+ // Supportive or unknown — less reactive to negative shocks
79
+ sensitivity = 0.2;
80
+ reason = `indirect exposure to "${event.headline}"`;
81
+ }
82
+ // Calculate belief shift
83
+ let impactShift;
84
+ if (direction === 0) {
85
+ // Mixed direction — push toward extremes based on existing position
86
+ impactShift = reaction.impact > 0
87
+ ? magnitude * sensitivity * propagation * 0.5
88
+ : -magnitude * sensitivity * propagation * 0.5;
89
+ reason += " (mixed signal — amplifies existing position)";
90
+ }
91
+ else {
92
+ impactShift = direction * magnitude * sensitivity * propagation;
93
+ }
94
+ // Confidence shift — shocks generally reduce confidence for non-targeted
95
+ const confidenceShift = isTargeted
96
+ ? 0.1 * propagation // targeted agents gain conviction
97
+ : -0.05 * magnitude * propagation; // others become less certain
98
+ // Apply shifts
99
+ const newImpact = Math.max(-1, Math.min(1, reaction.impact + impactShift));
100
+ const newConfidence = Math.max(0.05, Math.min(1, reaction.confidence + confidenceShift));
101
+ reaction.impact = Number(newImpact.toFixed(3));
102
+ reaction.confidence = Number(newConfidence.toFixed(3));
103
+ agentShifts.push({
104
+ stakeholder_id: reaction.stakeholder_id,
105
+ impactShift: Number(impactShift.toFixed(3)),
106
+ confidenceShift: Number(confidenceShift.toFixed(3)),
107
+ reason,
108
+ });
109
+ }
110
+ // Detect system-level effects
111
+ const totalShift = agentShifts.reduce((s, a) => s + Math.abs(a.impactShift), 0);
112
+ if (totalShift > reactions.length * 0.3) {
113
+ systemEffects.push("NARRATIVE CASCADE: event caused widespread belief shifts");
114
+ }
115
+ const polarization = reactions.filter(r => r.impact > 0.3).length > 0 &&
116
+ reactions.filter(r => r.impact < -0.3).length > 0;
117
+ if (polarization) {
118
+ systemEffects.push("POLARIZATION: event split agent consensus");
119
+ }
120
+ const consensus = Math.abs(reactions.reduce((s, r) => s + r.impact, 0) / reactions.length);
121
+ if (consensus > 0.5) {
122
+ systemEffects.push(`CONSENSUS SHIFT: agents aligned around ${consensus > 0 ? "positive" : "negative"} narrative`);
123
+ }
124
+ return { event, agentShifts, systemEffects };
125
+ }
126
+ // ============================================
127
+ // PRESET NARRATIVE EVENTS
128
+ // ============================================
129
+ exports.NARRATIVE_PRESETS = {
130
+ // Trading / Financial
131
+ tanker_explosion: {
132
+ id: "tanker_explosion",
133
+ headline: "Oil tanker hit by missile in Strait of Hormuz",
134
+ severity: "extreme",
135
+ targets: ["Energy", "Traders", "Financial", "Algorithmic"],
136
+ direction: "negative",
137
+ propagation: "viral",
138
+ category: "geopolitical",
139
+ },
140
+ rate_cut: {
141
+ id: "rate_cut",
142
+ headline: "Federal Reserve announces emergency rate cut",
143
+ severity: "major",
144
+ targets: ["Central Banks", "Institutional", "Financial", "Market"],
145
+ direction: "positive",
146
+ propagation: "viral",
147
+ category: "monetary",
148
+ },
149
+ bank_collapse: {
150
+ id: "bank_collapse",
151
+ headline: "Major bank reports insolvency — contagion fears",
152
+ severity: "extreme",
153
+ targets: ["Institutional", "Retail", "Financial", "Market Makers"],
154
+ direction: "negative",
155
+ propagation: "viral",
156
+ category: "financial",
157
+ },
158
+ flash_crash_rumor: {
159
+ id: "flash_crash_rumor",
160
+ headline: "Rumors of algorithmic trading malfunction spread",
161
+ severity: "moderate",
162
+ targets: ["Algorithmic", "Traders", "Retail"],
163
+ direction: "negative",
164
+ propagation: "normal",
165
+ category: "market",
166
+ },
167
+ sanctions: {
168
+ id: "sanctions",
169
+ headline: "New economic sanctions imposed on major oil producer",
170
+ severity: "major",
171
+ targets: ["Energy", "Government", "OPEC", "Oil"],
172
+ direction: "negative",
173
+ propagation: "normal",
174
+ category: "geopolitical",
175
+ },
176
+ // Geopolitical
177
+ taiwan_exercises: {
178
+ id: "taiwan_exercises",
179
+ headline: "China begins military exercises near Taiwan",
180
+ severity: "major",
181
+ targets: ["Military", "Government", "Financial", "Traders"],
182
+ direction: "negative",
183
+ propagation: "viral",
184
+ category: "geopolitical",
185
+ },
186
+ diplomatic_breakthrough: {
187
+ id: "diplomatic_breakthrough",
188
+ headline: "Surprise diplomatic agreement reached — tensions ease",
189
+ severity: "major",
190
+ targets: ["Government", "Military", "Financial"],
191
+ direction: "positive",
192
+ propagation: "normal",
193
+ category: "geopolitical",
194
+ },
195
+ ceasefire: {
196
+ id: "ceasefire",
197
+ headline: "Ceasefire announced — shipping lanes reopening",
198
+ severity: "moderate",
199
+ targets: ["Energy", "Military", "Government", "Consumers"],
200
+ direction: "positive",
201
+ propagation: "normal",
202
+ category: "geopolitical",
203
+ },
204
+ // Political / Regulatory
205
+ candidate_indicted: {
206
+ id: "candidate_indicted",
207
+ headline: "Leading candidate indicted on corruption charges",
208
+ severity: "major",
209
+ targets: ["Politicians", "Consumers", "Media"],
210
+ direction: "mixed",
211
+ propagation: "viral",
212
+ category: "political",
213
+ },
214
+ regulation_shock: {
215
+ id: "regulation_shock",
216
+ headline: "Unexpected strict AI regulation passed overnight",
217
+ severity: "major",
218
+ targets: ["Regulators", "EV", "Grid", "AI"],
219
+ direction: "negative",
220
+ propagation: "normal",
221
+ category: "regulatory",
222
+ },
223
+ stimulus_package: {
224
+ id: "stimulus_package",
225
+ headline: "Emergency fiscal stimulus package announced",
226
+ severity: "moderate",
227
+ targets: ["Government", "Consumers", "Financial"],
228
+ direction: "positive",
229
+ propagation: "normal",
230
+ category: "economic",
231
+ },
232
+ // Energy
233
+ grid_failure: {
234
+ id: "grid_failure",
235
+ headline: "Regional power grid failure — rolling blackouts",
236
+ severity: "major",
237
+ targets: ["Grid", "Consumers", "EV", "Energy"],
238
+ direction: "negative",
239
+ propagation: "normal",
240
+ category: "infrastructure",
241
+ },
242
+ oil_discovery: {
243
+ id: "oil_discovery",
244
+ headline: "Major new oil field discovered — supply outlook shifts",
245
+ severity: "moderate",
246
+ targets: ["Energy", "Oil", "OPEC", "Consumers"],
247
+ direction: "positive",
248
+ propagation: "slow",
249
+ category: "energy",
250
+ },
251
+ };
252
+ // ============================================
253
+ // CLI ARGUMENT PARSING
254
+ // ============================================
255
+ /**
256
+ * Parse --inject arguments into NarrativeEvent arrays.
257
+ *
258
+ * Format: --inject event_id@round[,event_id@round,...]
259
+ * Or: --inject "Custom headline"@round
260
+ *
261
+ * Examples:
262
+ * --inject tanker_explosion@5
263
+ * --inject rate_cut@3,sanctions@6
264
+ * --inject "Bank collapses"@8
265
+ */
266
+ function parseInjectArgs(args) {
267
+ const injectIdx = args.indexOf("--inject");
268
+ if (injectIdx === -1 || !args[injectIdx + 1])
269
+ return [];
270
+ const injectStr = args[injectIdx + 1];
271
+ const events = [];
272
+ for (const part of injectStr.split(",")) {
273
+ const atIdx = part.lastIndexOf("@");
274
+ if (atIdx === -1)
275
+ continue;
276
+ const eventId = part.slice(0, atIdx).trim();
277
+ const round = parseInt(part.slice(atIdx + 1), 10);
278
+ if (isNaN(round))
279
+ continue;
280
+ const preset = exports.NARRATIVE_PRESETS[eventId];
281
+ if (preset) {
282
+ events.push({ ...preset, round });
283
+ }
284
+ else {
285
+ // Custom headline
286
+ events.push({
287
+ id: `custom_${round}`,
288
+ headline: eventId.replace(/^["']|["']$/g, ""),
289
+ round,
290
+ severity: "major",
291
+ targets: [],
292
+ direction: "mixed",
293
+ propagation: "normal",
294
+ category: "custom",
295
+ });
296
+ }
297
+ }
298
+ return events;
299
+ }
300
+ /**
301
+ * Get events scheduled for a specific round.
302
+ */
303
+ function getEventsForRound(events, round) {
304
+ return events.filter(e => e.round === round);
305
+ }
@@ -444,13 +444,67 @@ async function processReasonRequest(request) {
444
444
  ...governanceResult.enforcedConstraints,
445
445
  ];
446
446
  // Build NeuroverseOS governance summary
447
+ // Compute world_health_score from actual simulation dynamics, not static validation
448
+ let dynamicWorldHealth;
449
+ if (swarmResult) {
450
+ const allReactions = swarmResult.rounds.flatMap((r) => r.reactions ?? []);
451
+ if (allReactions.length > 0) {
452
+ const allImpacts = allReactions.map((r) => r.impact ?? 0);
453
+ const avgImpact = allImpacts.reduce((s, i) => s + i, 0) / allImpacts.length;
454
+ const maxVolatility = Math.max(...allImpacts.map((i) => Math.abs(i)));
455
+ const impactVariance = allImpacts.reduce((s, i) => s + (i - avgImpact) ** 2, 0) / allImpacts.length;
456
+ // Stability = f(variance, volatility, trajectory, negative sentiment)
457
+ let stability = 1 - Math.min(1, impactVariance * 3 + Math.max(0, -avgImpact));
458
+ if (swarmResult.trajectory === "converging" || swarmResult.trajectory === "stabilizing") {
459
+ stability = Math.min(1, stability + 0.15);
460
+ }
461
+ if (swarmResult.trajectory === "escalating") {
462
+ stability = Math.max(0, stability - 0.2);
463
+ }
464
+ if (swarmResult.trajectory === "diverging") {
465
+ stability = Math.max(0, stability - 0.15);
466
+ }
467
+ // Factor in governance interventions
468
+ const enforcedCount = enforcedConstraints.length;
469
+ stability = Math.min(1, stability + enforcedCount * 0.02);
470
+ dynamicWorldHealth = Math.round(Math.max(0, Math.min(100, stability * 100)));
471
+ }
472
+ }
473
+ // Compute dynamic collapse risk from simulation
474
+ let dynamicCollapseRisk;
475
+ if (swarmResult) {
476
+ const allReactions = swarmResult.rounds.flatMap((r) => r.reactions ?? []);
477
+ if (allReactions.length > 0) {
478
+ const allImpacts = allReactions.map((r) => r.impact ?? 0);
479
+ const avgImpact = allImpacts.reduce((s, i) => s + i, 0) / allImpacts.length;
480
+ const maxVol = Math.max(...allImpacts.map((i) => Math.abs(i)));
481
+ let risk = 0;
482
+ if (swarmResult.trajectory === "escalating")
483
+ risk += 0.35;
484
+ if (swarmResult.trajectory === "diverging")
485
+ risk += 0.25;
486
+ if (avgImpact < -0.3)
487
+ risk += 0.25;
488
+ if (maxVol > 0.7)
489
+ risk += 0.15;
490
+ risk = Math.min(0.95, risk);
491
+ if (risk > 0.5)
492
+ dynamicCollapseRisk = "high";
493
+ else if (risk > 0.3)
494
+ dynamicCollapseRisk = "moderate";
495
+ else if (risk > 0.1)
496
+ dynamicCollapseRisk = "low";
497
+ else
498
+ dynamicCollapseRisk = "none";
499
+ }
500
+ }
447
501
  const neuroverseTrace = governanceResult.guardVerdict ? {
448
502
  guard_status: governanceResult.guardVerdict.status,
449
503
  world_viability: governanceResult.governanceSignals?.viability,
450
504
  world_collapsed: governanceResult.governanceSignals?.collapsed,
451
- collapse_risk: governanceResult.governanceSignals?.collapseRisk,
452
- rules_fired: governanceResult.governanceSignals?.rulesFired,
453
- world_health_score: governanceResult.validationReport?.summary.completenessScore,
505
+ collapse_risk: dynamicCollapseRisk ?? governanceResult.governanceSignals?.collapseRisk,
506
+ rules_fired: governanceResult.governanceSignals?.rulesFired ?? enforcedConstraints.length,
507
+ world_health_score: dynamicWorldHealth ?? governanceResult.validationReport?.summary.completenessScore,
454
508
  invariant_coverage: governanceResult.validationReport?.summary.invariantCoverage,
455
509
  } : undefined;
456
510
  const governance = {
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Governed Report Engine
4
+ *
5
+ * Generates structured reports from simulation traces.
6
+ * ALL report generation goes through /world evaluate — even AI-generated reports.
7
+ *
8
+ * "Even the AI has to follow the rules."
9
+ *
10
+ * Pipeline:
11
+ * Trace (ground truth)
12
+ * → /world evaluate (ai_analyst actor)
13
+ * → governed constraints applied
14
+ * → structured report output
15
+ * → trace recorded
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.extractTrace = extractTrace;
19
+ exports.generateGovernedReport = generateGovernedReport;
20
+ const aiProvider_1 = require("./aiProvider");
21
+ // ============================================
22
+ // TRACE EXTRACTION
23
+ // ============================================
24
+ /**
25
+ * Extract a SimulationTraceInput from a GovernedComparisonResult.
26
+ * This is the "ground truth" that the report must reference.
27
+ */
28
+ function extractTrace(comparison) {
29
+ const governed = comparison.governed;
30
+ const stats = comparison.governanceStats;
31
+ const rounds = governed.swarm.rounds.map((r, i) => ({
32
+ round: r.round,
33
+ reactions: r.reactions.map(rx => ({
34
+ agent: rx.stakeholder_id,
35
+ action: rx.reaction,
36
+ impact: rx.impact,
37
+ verdict: undefined,
38
+ })),
39
+ interventions: r.emergent_dynamics
40
+ ?.filter(d => d.includes("[BLOCK]") || d.includes("[PAUSE]") || d.includes("CIRCUIT BREAKER") || d.includes("intervention"))
41
+ ?? [],
42
+ }));
43
+ // Collect all interventions from emergent dynamics
44
+ const allInterventions = governed.swarm.rounds.flatMap(r => (r.emergent_dynamics ?? []).filter(d => d.includes("[BLOCK]") || d.includes("[PAUSE]") || d.includes("GATE") || d.includes("Rebalanced") || d.includes("Capped")));
45
+ const metrics = {
46
+ avgImpact: governed.metrics.avgImpact,
47
+ collapseProbability: governed.metrics.collapseProbability,
48
+ stabilityScore: governed.metrics.stabilityScore,
49
+ maxVolatility: governed.metrics.maxVolatility,
50
+ peakNegativeSentiment: governed.metrics.peakNegativeSentiment,
51
+ };
52
+ return {
53
+ scenario: comparison.scenario,
54
+ rounds,
55
+ metrics,
56
+ interventions: allInterventions,
57
+ governanceStats: {
58
+ totalEvaluations: stats.totalEvaluations,
59
+ blocks: stats.verdicts.block,
60
+ pauses: stats.verdicts.pause,
61
+ allows: stats.verdicts.allow,
62
+ rulesFired: stats.rulesFired,
63
+ },
64
+ };
65
+ }
66
+ /**
67
+ * Generate a governed report from a simulation comparison.
68
+ *
69
+ * This is the main entry point. It:
70
+ * 1. Extracts the trace (ground truth)
71
+ * 2. Routes through /world evaluate (ai_analyst actor)
72
+ * 3. Validates governance constraints
73
+ * 4. Returns structured report with governance trace
74
+ */
75
+ async function generateGovernedReport(comparison, options = {}) {
76
+ const trace = extractTrace(comparison);
77
+ const aiEnabled = options.aiEnabled ?? false;
78
+ const providerName = aiEnabled ? (options.provider ?? "deterministic") : "deterministic";
79
+ const provider = (0, aiProvider_1.getAIProvider)(providerName);
80
+ // Find the ai_analyst role
81
+ const analystRole = aiProvider_1.AI_ROLES.find(r => r.id === "ai_analyst");
82
+ // Route through governance: /world evaluate
83
+ const governance = await (0, aiProvider_1.evaluateAIAction)(analystRole, "generate_report", async () => {
84
+ if (aiEnabled && providerName !== "deterministic") {
85
+ // AI provider generates report — still governed
86
+ return provider.summarize(trace);
87
+ }
88
+ // Deterministic: generate from trace data only
89
+ return (0, aiProvider_1.generateDeterministicReport)(trace);
90
+ });
91
+ return {
92
+ report: governance.result,
93
+ governance,
94
+ aiUsed: aiEnabled && providerName !== "deterministic",
95
+ provider: providerName,
96
+ };
97
+ }