@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.
Files changed (52) hide show
  1. package/README.md +376 -66
  2. package/dist/adapters/mirofish.js +461 -0
  3. package/dist/adapters/scienceclaw.js +750 -0
  4. package/dist/assets/index-CHmUN8s0.js +532 -0
  5. package/dist/assets/index-DWgMnB7I.css +1 -0
  6. package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
  7. package/dist/assets/reportEngine-BVdQ2_nW.js +1 -0
  8. package/dist/components/ConstraintsPanel.js +11 -0
  9. package/dist/components/StakeholderBuilder.js +32 -0
  10. package/dist/components/ui/badge.js +24 -0
  11. package/dist/components/ui/button.js +70 -0
  12. package/dist/components/ui/card.js +57 -0
  13. package/dist/components/ui/input.js +44 -0
  14. package/dist/components/ui/label.js +45 -0
  15. package/dist/components/ui/select.js +70 -0
  16. package/dist/engine/aiProvider.js +681 -0
  17. package/dist/engine/auditTrace.js +352 -0
  18. package/dist/engine/behavioralAnalysis.js +605 -0
  19. package/dist/engine/cli.js +1408 -299
  20. package/dist/engine/dynamicsGovernance.js +588 -0
  21. package/dist/engine/fullGovernedLoop.js +367 -0
  22. package/dist/engine/governance.js +8 -3
  23. package/dist/engine/governedSimulation.js +114 -17
  24. package/dist/engine/index.js +56 -1
  25. package/dist/engine/liveAdapter.js +342 -0
  26. package/dist/engine/liveVisualizer.js +3063 -0
  27. package/dist/engine/metrics/science.metrics.js +335 -0
  28. package/dist/engine/narrativeInjection.js +305 -0
  29. package/dist/engine/policyEnforcement.js +1611 -0
  30. package/dist/engine/policyEngine.js +799 -0
  31. package/dist/engine/primeRadiant.js +540 -0
  32. package/dist/engine/reasoningEngine.js +57 -3
  33. package/dist/engine/reportEngine.js +97 -0
  34. package/dist/engine/scenarioComparison.js +463 -0
  35. package/dist/engine/scenarioLibrary.js +231 -0
  36. package/dist/engine/swarmSimulation.js +54 -1
  37. package/dist/engine/worldComparison.js +358 -0
  38. package/dist/engine/worldStorage.js +232 -0
  39. package/dist/favicon.ico +0 -0
  40. package/dist/index.html +23 -0
  41. package/dist/lib/reasoningEngine.js +290 -0
  42. package/dist/lib/simulationAdapter.js +686 -0
  43. package/dist/lib/swarmParser.js +291 -0
  44. package/dist/lib/types.js +2 -0
  45. package/dist/lib/utils.js +8 -0
  46. package/dist/placeholder.svg +1 -0
  47. package/dist/robots.txt +14 -0
  48. package/dist/runtime/govern.js +473 -0
  49. package/dist/runtime/index.js +75 -0
  50. package/dist/runtime/types.js +11 -0
  51. package/package.json +17 -12
  52. package/variants/.gitkeep +0 -0
@@ -0,0 +1,588 @@
1
+ "use strict";
2
+ /**
3
+ * Dynamics Governance Engine — System-Level Per-Round Control
4
+ *
5
+ * This is the second govern() call.
6
+ *
7
+ * Layer A: govern(action) — controls what agents DO (per-action, already built)
8
+ * Layer B: governDynamics() — controls how actions COMPOUND (per-round, this file)
9
+ *
10
+ * "We don't just control what agents do.
11
+ * We control how their actions shape the system over time."
12
+ *
13
+ * Three dynamics controllers:
14
+ * 1. Propagation Controller — governs how far actions spread
15
+ * 2. Amplification Engine — governs media/influence weight modulation
16
+ * 3. Cascade Circuit Breaker — governs feedback loops and system-level states
17
+ * 4. Trust-Responsive Visibility — governs the information environment
18
+ *
19
+ * Usage:
20
+ * const dynamics = createDynamicsGovernor(policyText)
21
+ * for (const round of simulation) {
22
+ * // ... agents act, govern() filters actions ...
23
+ * const result = dynamics.governRound(reactions, systemState, round)
24
+ * // result.reactions — modified by dynamics rules
25
+ * // result.systemState — updated state after dynamics
26
+ * // result.interventions — what dynamics did this round
27
+ * }
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.createDynamicsGovernor = createDynamicsGovernor;
31
+ exports.governDynamics = governDynamics;
32
+ const policyEngine_1 = require("./policyEngine");
33
+ // ============================================
34
+ // SYSTEM STATE COMPUTATION
35
+ // ============================================
36
+ function computeSystemMetrics(reactions, prevState) {
37
+ const impacts = reactions.map((r) => r.impact);
38
+ const avgImpact = impacts.reduce((s, i) => s + i, 0) / impacts.length;
39
+ const variance = impacts.reduce((s, i) => s + (i - avgImpact) ** 2, 0) / impacts.length;
40
+ // Polarization: high when agents are split into opposing camps
41
+ const positive = impacts.filter((i) => i > 0.2).length;
42
+ const negative = impacts.filter((i) => i < -0.2).length;
43
+ const total = impacts.length;
44
+ const polarization = total > 0
45
+ ? Math.min(1, (Math.min(positive, negative) / (total * 0.5)) * (variance * 4))
46
+ : 0;
47
+ // Outrage: high when there's strong negative sentiment
48
+ const negativeIntensity = impacts
49
+ .filter((i) => i < 0)
50
+ .reduce((s, i) => s + Math.abs(i), 0) / Math.max(1, impacts.length);
51
+ const outrage = Math.min(1, negativeIntensity * 1.5);
52
+ return { polarization, outrage, avgImpact, variance };
53
+ }
54
+ function detectTrend(history, metric, lookback = 3) {
55
+ if (history.length < 2)
56
+ return "stable";
57
+ const recent = history.slice(-lookback);
58
+ if (recent.length < 2)
59
+ return "stable";
60
+ const first = recent[0][metric];
61
+ const last = recent[recent.length - 1][metric];
62
+ const delta = last - first;
63
+ if (delta > 0.1)
64
+ return "rising";
65
+ if (delta < -0.1)
66
+ return "falling";
67
+ return "stable";
68
+ }
69
+ // ============================================
70
+ // DYNAMICS RULE PARSING
71
+ // ============================================
72
+ function parseDynamicsRules(policyText) {
73
+ const parsed = (0, policyEngine_1.parseRulesFromText)(policyText);
74
+ const rules = [];
75
+ let ruleIdx = 0;
76
+ for (const rule of parsed.rules) {
77
+ const desc = rule.description.toLowerCase();
78
+ // Propagation rules
79
+ if (desc.includes("spread") || desc.includes("propagat") ||
80
+ desc.includes("viral") || desc.includes("limit reach") ||
81
+ desc.includes("contain") || desc.includes("quarantine")) {
82
+ rules.push({
83
+ id: `DYN-PROP-${++ruleIdx}`,
84
+ description: rule.description,
85
+ type: "propagation",
86
+ threshold: 0.6,
87
+ strength: rule.category === "prohibit" ? 0.8 : rule.category === "limit" ? 0.5 : 0.3,
88
+ monitors: "informationVelocity",
89
+ active: true,
90
+ });
91
+ }
92
+ // Amplification rules
93
+ if (desc.includes("amplif") || desc.includes("media") ||
94
+ desc.includes("influence") || desc.includes("boost") ||
95
+ desc.includes("visibil") || desc.includes("platform")) {
96
+ rules.push({
97
+ id: `DYN-AMP-${++ruleIdx}`,
98
+ description: rule.description,
99
+ type: "amplification",
100
+ threshold: 0.5,
101
+ strength: rule.category === "prohibit" ? 0.7 : 0.4,
102
+ monitors: "amplification",
103
+ active: true,
104
+ });
105
+ }
106
+ // Cascade / circuit breaker rules
107
+ if (desc.includes("cascade") || desc.includes("circuit") ||
108
+ desc.includes("runaway") || desc.includes("feedback") ||
109
+ desc.includes("spiral") || desc.includes("chain reaction") ||
110
+ desc.includes("contagion") || desc.includes("panic")) {
111
+ rules.push({
112
+ id: `DYN-CASC-${++ruleIdx}`,
113
+ description: rule.description,
114
+ type: "cascade",
115
+ threshold: 0.7,
116
+ strength: 0.9,
117
+ monitors: "cascadeRisk",
118
+ active: true,
119
+ });
120
+ }
121
+ // Trust rules
122
+ if (desc.includes("trust") || desc.includes("legitim") ||
123
+ desc.includes("credib") || desc.includes("transparen") ||
124
+ desc.includes("accountab") || desc.includes("institution")) {
125
+ rules.push({
126
+ id: `DYN-TRUST-${++ruleIdx}`,
127
+ description: rule.description,
128
+ type: "trust",
129
+ threshold: 0.4,
130
+ strength: 0.5,
131
+ monitors: "trust",
132
+ active: true,
133
+ });
134
+ }
135
+ // Feedback dampening rules
136
+ if (desc.includes("dampen") || desc.includes("cool") ||
137
+ desc.includes("de-escalat") || desc.includes("moderate") ||
138
+ desc.includes("calm") || desc.includes("stabiliz")) {
139
+ rules.push({
140
+ id: `DYN-FEED-${++ruleIdx}`,
141
+ description: rule.description,
142
+ type: "feedback",
143
+ threshold: 0.5,
144
+ strength: 0.6,
145
+ monitors: "outrage",
146
+ active: true,
147
+ });
148
+ }
149
+ // Cooling period rules
150
+ if (desc.includes("pause") || desc.includes("halt") ||
151
+ desc.includes("freeze") || desc.includes("cooling") ||
152
+ desc.includes("moratorium") || desc.includes("waiting period")) {
153
+ rules.push({
154
+ id: `DYN-COOL-${++ruleIdx}`,
155
+ description: rule.description,
156
+ type: "cooling",
157
+ threshold: 0.7,
158
+ strength: 0.8,
159
+ monitors: "outrage",
160
+ active: true,
161
+ });
162
+ }
163
+ }
164
+ // Always add baseline dynamics rules if none were parsed
165
+ if (rules.length === 0) {
166
+ rules.push({
167
+ id: "DYN-BASE-1",
168
+ description: "Default propagation control — limit information spread during high outrage",
169
+ type: "propagation",
170
+ threshold: 0.7,
171
+ strength: 0.4,
172
+ monitors: "informationVelocity",
173
+ active: true,
174
+ }, {
175
+ id: "DYN-BASE-2",
176
+ description: "Default cascade protection — activate circuit breaker during crisis",
177
+ type: "cascade",
178
+ threshold: 0.8,
179
+ strength: 0.7,
180
+ monitors: "cascadeRisk",
181
+ active: true,
182
+ });
183
+ }
184
+ return rules;
185
+ }
186
+ // ============================================
187
+ // CORE: createDynamicsGovernor()
188
+ // ============================================
189
+ function createDynamicsGovernor(policyText, initialState, options) {
190
+ const dynamicsRules = options?.skipRules ? [] : parseDynamicsRules(policyText);
191
+ let state = {
192
+ polarization: 0.1,
193
+ outrage: 0.1,
194
+ trust: 0.7,
195
+ informationVelocity: 0.3,
196
+ amplification: 1.0,
197
+ cascadeRisk: 0.1,
198
+ contagionDepth: 2,
199
+ coolingPeriod: 0,
200
+ history: [],
201
+ ...initialState,
202
+ };
203
+ const stats = {
204
+ totalRounds: 0,
205
+ totalInterventions: 0,
206
+ propagationLimits: 0,
207
+ amplificationDampens: 0,
208
+ cascadeBreakers: 0,
209
+ coolingPeriods: 0,
210
+ trustBoosts: 0,
211
+ peakOutrage: 0,
212
+ peakPolarization: 0,
213
+ peakCascadeRisk: 0,
214
+ trustSum: 0,
215
+ };
216
+ function governRound(reactions, round, agentTypes) {
217
+ stats.totalRounds++;
218
+ const interventions = [];
219
+ const rulesFired = [];
220
+ const governed = reactions.map((r) => ({ ...r }));
221
+ // --- Step 1: Compute current system metrics from reactions ---
222
+ const metrics = computeSystemMetrics(reactions, state);
223
+ // Update state from observed metrics
224
+ state.polarization = state.polarization * 0.6 + metrics.polarization * 0.4;
225
+ state.outrage = state.outrage * 0.5 + metrics.outrage * 0.5;
226
+ state.informationVelocity = Math.min(1, 0.3 + metrics.variance * 2);
227
+ state.cascadeRisk = Math.min(1, state.outrage * 0.3 +
228
+ state.polarization * 0.2 +
229
+ state.informationVelocity * 0.2 +
230
+ (1 - state.trust) * 0.3);
231
+ // Trust erodes when outrage is high, recovers when calm
232
+ if (state.outrage > 0.5) {
233
+ state.trust = Math.max(0.05, state.trust - 0.05 * state.outrage);
234
+ }
235
+ else if (state.outrage < 0.2) {
236
+ state.trust = Math.min(1, state.trust + 0.02);
237
+ }
238
+ // Track peaks
239
+ stats.peakOutrage = Math.max(stats.peakOutrage, state.outrage);
240
+ stats.peakPolarization = Math.max(stats.peakPolarization, state.polarization);
241
+ stats.peakCascadeRisk = Math.max(stats.peakCascadeRisk, state.cascadeRisk);
242
+ stats.trustSum += state.trust;
243
+ // --- Step 2: Apply cooling period (from previous round's cascade breaker) ---
244
+ if (state.coolingPeriod > 0) {
245
+ const coolingStrength = 0.4 + (state.coolingPeriod * 0.1);
246
+ for (const agent of governed) {
247
+ if (agent.impact < -0.1) {
248
+ const before = agent.impact;
249
+ agent.impact = agent.impact * (1 - coolingStrength);
250
+ agent.confidence = Math.max(0.1, agent.confidence * 0.7);
251
+ }
252
+ }
253
+ interventions.push({
254
+ type: "COOLING_PERIOD",
255
+ description: `Cooling period active (${state.coolingPeriod} rounds remaining) — reactive posting dampened by ${(coolingStrength * 100).toFixed(0)}%`,
256
+ triggeredBy: "COOLING_PERIOD",
257
+ agentsAffected: governed.filter((a) => a.impact < -0.1).length,
258
+ magnitude: coolingStrength,
259
+ effect: { metric: "outrage", before: state.outrage, after: state.outrage * (1 - coolingStrength * 0.3) },
260
+ });
261
+ state.coolingPeriod--;
262
+ stats.coolingPeriods++;
263
+ }
264
+ // --- Step 3: Apply each dynamics rule ---
265
+ for (const rule of dynamicsRules) {
266
+ if (!rule.active)
267
+ continue;
268
+ const currentValue = state[rule.monitors];
269
+ switch (rule.type) {
270
+ // ========== PROPAGATION CONTROL ==========
271
+ case "propagation": {
272
+ if (state.informationVelocity > rule.threshold || state.outrage > 0.6) {
273
+ const dampFactor = 1 - (rule.strength * 0.5);
274
+ let affected = 0;
275
+ // Limit propagation depth
276
+ const prevDepth = state.contagionDepth;
277
+ state.contagionDepth = Math.max(1, Math.floor(state.contagionDepth * dampFactor));
278
+ // Reduce the impact of agents whose reactions are extreme
279
+ // (simulates limiting how far their content spreads)
280
+ for (const agent of governed) {
281
+ if (Math.abs(agent.impact) > 0.4) {
282
+ const before = agent.impact;
283
+ agent.impact = agent.impact * dampFactor;
284
+ affected++;
285
+ }
286
+ }
287
+ if (affected > 0) {
288
+ const beforeVelocity = state.informationVelocity;
289
+ state.informationVelocity *= dampFactor;
290
+ interventions.push({
291
+ type: "PROPAGATION_LIMIT",
292
+ description: `Propagation limited: spread depth ${prevDepth} → ${state.contagionDepth}, ${affected} extreme agents dampened (${rule.description})`,
293
+ triggeredBy: rule.id,
294
+ agentsAffected: affected,
295
+ magnitude: rule.strength,
296
+ effect: { metric: "informationVelocity", before: beforeVelocity, after: state.informationVelocity },
297
+ });
298
+ rulesFired.push(rule.id);
299
+ stats.propagationLimits++;
300
+ stats.totalInterventions++;
301
+ }
302
+ }
303
+ break;
304
+ }
305
+ // ========== AMPLIFICATION DAMPENING ==========
306
+ case "amplification": {
307
+ const polarizationTrend = detectTrend(state.history, "polarization");
308
+ if (state.polarization > rule.threshold || polarizationTrend === "rising") {
309
+ const dampFactor = 1 - (rule.strength * 0.4);
310
+ let affected = 0;
311
+ // Reduce amplification for media/influencer type agents
312
+ for (const agent of governed) {
313
+ const agentType = agentTypes?.get(agent.stakeholder_id) ?? "";
314
+ const isAmplifier = agentType.toLowerCase().includes("media") ||
315
+ agentType.toLowerCase().includes("influencer") ||
316
+ agent.stakeholder_id.toLowerCase().includes("media") ||
317
+ agent.stakeholder_id.toLowerCase().includes("记者") || // journalist (Chinese)
318
+ agent.stakeholder_id.toLowerCase().includes("媒") || // media (Chinese)
319
+ Math.abs(agent.impact) > 0.6; // or any agent with extreme impact
320
+ if (isAmplifier) {
321
+ const before = agent.impact;
322
+ agent.impact = agent.impact * dampFactor;
323
+ agent.confidence = Math.max(0.15, agent.confidence * 0.8);
324
+ affected++;
325
+ }
326
+ }
327
+ if (affected > 0) {
328
+ const beforeAmp = state.amplification;
329
+ state.amplification *= dampFactor;
330
+ interventions.push({
331
+ type: "AMPLIFICATION_DAMPEN",
332
+ description: `Amplification dampened: ${affected} high-influence agents reduced by ${((1 - dampFactor) * 100).toFixed(0)}% (polarization ${state.polarization.toFixed(2)}, trend: ${polarizationTrend}) — ${rule.description}`,
333
+ triggeredBy: rule.id,
334
+ agentsAffected: affected,
335
+ magnitude: rule.strength,
336
+ effect: { metric: "amplification", before: beforeAmp, after: state.amplification },
337
+ });
338
+ rulesFired.push(rule.id);
339
+ stats.amplificationDampens++;
340
+ stats.totalInterventions++;
341
+ }
342
+ }
343
+ break;
344
+ }
345
+ // ========== CASCADE CIRCUIT BREAKER ==========
346
+ case "cascade": {
347
+ if (state.cascadeRisk > rule.threshold) {
348
+ const outrageRising = detectTrend(state.history, "outrage") === "rising";
349
+ const breakerStrength = outrageRising ? rule.strength : rule.strength * 0.6;
350
+ let affected = 0;
351
+ // Compress ALL negative reactions toward zero
352
+ for (const agent of governed) {
353
+ if (agent.impact < -0.15) {
354
+ const before = agent.impact;
355
+ agent.impact = agent.impact * (1 - breakerStrength);
356
+ agent.confidence = Math.max(0.1, agent.confidence * (1 - breakerStrength * 0.5));
357
+ affected++;
358
+ }
359
+ }
360
+ // Activate cooling period
361
+ if (state.coolingPeriod === 0 && outrageRising) {
362
+ state.coolingPeriod = 2; // 2 rounds of cooling
363
+ }
364
+ const beforeCascade = state.cascadeRisk;
365
+ state.cascadeRisk *= (1 - breakerStrength * 0.4);
366
+ interventions.push({
367
+ type: "CASCADE_BREAKER",
368
+ description: `CASCADE CIRCUIT BREAKER: ${affected} agents compressed, cascade risk ${beforeCascade.toFixed(2)} → ${state.cascadeRisk.toFixed(2)}${outrageRising ? " + cooling period activated" : ""} — ${rule.description}`,
369
+ triggeredBy: rule.id,
370
+ agentsAffected: affected,
371
+ magnitude: breakerStrength,
372
+ effect: { metric: "cascadeRisk", before: beforeCascade, after: state.cascadeRisk },
373
+ });
374
+ rulesFired.push(rule.id);
375
+ stats.cascadeBreakers++;
376
+ stats.totalInterventions++;
377
+ }
378
+ break;
379
+ }
380
+ // ========== TRUST-RESPONSIVE VISIBILITY ==========
381
+ case "trust": {
382
+ if (state.trust < rule.threshold) {
383
+ let affected = 0;
384
+ // Boost official/institutional agents, dampen hostile/unknown ones
385
+ for (const agent of governed) {
386
+ const agentType = agentTypes?.get(agent.stakeholder_id) ?? "";
387
+ const isOfficial = agentType.toLowerCase().includes("official") ||
388
+ agentType.toLowerCase().includes("government") ||
389
+ agentType.toLowerCase().includes("institution") ||
390
+ agent.stakeholder_id.toLowerCase().includes("regulator") ||
391
+ agent.stakeholder_id.toLowerCase().includes("central") ||
392
+ agent.stakeholder_id.toLowerCase().includes("official") ||
393
+ agent.stakeholder_id.toLowerCase().includes("管理") || // management (Chinese)
394
+ agent.stakeholder_id.toLowerCase().includes("官方"); // official (Chinese)
395
+ if (isOfficial && agent.impact > -0.2) {
396
+ // Boost visibility of official sources
397
+ const before = agent.impact;
398
+ agent.impact = Math.min(1, agent.impact * 1.3 + 0.1);
399
+ agent.confidence = Math.min(1, agent.confidence * 1.2);
400
+ affected++;
401
+ }
402
+ else if (!isOfficial && agent.impact < -0.3) {
403
+ // Reduce visibility of unverified negative claims
404
+ agent.impact = agent.impact * 0.7;
405
+ affected++;
406
+ }
407
+ }
408
+ if (affected > 0) {
409
+ const beforeTrust = state.trust;
410
+ state.trust = Math.min(1, state.trust + 0.03 * rule.strength);
411
+ interventions.push({
412
+ type: "TRUST_BOOST",
413
+ description: `Trust-responsive visibility: official sources boosted, unverified claims dampened (trust ${beforeTrust.toFixed(2)} → ${state.trust.toFixed(2)}) — ${rule.description}`,
414
+ triggeredBy: rule.id,
415
+ agentsAffected: affected,
416
+ magnitude: rule.strength,
417
+ effect: { metric: "trust", before: beforeTrust, after: state.trust },
418
+ });
419
+ rulesFired.push(rule.id);
420
+ stats.trustBoosts++;
421
+ stats.totalInterventions++;
422
+ }
423
+ }
424
+ break;
425
+ }
426
+ // ========== FEEDBACK DAMPENING ==========
427
+ case "feedback": {
428
+ const outrageTrend = detectTrend(state.history, "outrage");
429
+ if (state.outrage > rule.threshold && outrageTrend === "rising") {
430
+ const dampFactor = 1 - (rule.strength * 0.35);
431
+ let affected = 0;
432
+ // Reduce ALL reactive behavior — pull everything toward zero
433
+ for (const agent of governed) {
434
+ if (Math.abs(agent.impact) > 0.2) {
435
+ const before = agent.impact;
436
+ agent.impact = agent.impact * dampFactor;
437
+ affected++;
438
+ }
439
+ }
440
+ if (affected > 0) {
441
+ const beforeOutrage = state.outrage;
442
+ state.outrage *= dampFactor;
443
+ interventions.push({
444
+ type: "FEEDBACK_DAMPEN",
445
+ description: `Feedback loop dampened: ${affected} reactive agents moderated, outrage ${beforeOutrage.toFixed(2)} → ${state.outrage.toFixed(2)} (trend: ${outrageTrend}) — ${rule.description}`,
446
+ triggeredBy: rule.id,
447
+ agentsAffected: affected,
448
+ magnitude: rule.strength,
449
+ effect: { metric: "outrage", before: beforeOutrage, after: state.outrage },
450
+ });
451
+ rulesFired.push(rule.id);
452
+ stats.totalInterventions++;
453
+ }
454
+ }
455
+ break;
456
+ }
457
+ // ========== COOLING PERIOD TRIGGER ==========
458
+ case "cooling": {
459
+ if (state.outrage > rule.threshold && state.coolingPeriod === 0) {
460
+ state.coolingPeriod = Math.ceil(rule.strength * 3); // 1-3 rounds
461
+ interventions.push({
462
+ type: "COOLING_PERIOD",
463
+ description: `Cooling period triggered: ${state.coolingPeriod} round(s) of dampened reactive posting (outrage: ${state.outrage.toFixed(2)}) — ${rule.description}`,
464
+ triggeredBy: rule.id,
465
+ agentsAffected: 0,
466
+ magnitude: rule.strength,
467
+ effect: { metric: "outrage", before: state.outrage, after: state.outrage },
468
+ });
469
+ rulesFired.push(rule.id);
470
+ stats.coolingPeriods++;
471
+ stats.totalInterventions++;
472
+ }
473
+ break;
474
+ }
475
+ }
476
+ }
477
+ // --- Step 4: Reclamp all impacts ---
478
+ for (const agent of governed) {
479
+ agent.impact = Math.max(-1, Math.min(1, agent.impact));
480
+ agent.impact = Number(agent.impact.toFixed(3));
481
+ agent.confidence = Math.max(0.05, Math.min(1, agent.confidence));
482
+ agent.confidence = Number(agent.confidence.toFixed(3));
483
+ }
484
+ // --- Step 5: Record state snapshot ---
485
+ const postMetrics = computeSystemMetrics(governed, state);
486
+ state.history.push({
487
+ round,
488
+ polarization: Number(state.polarization.toFixed(3)),
489
+ outrage: Number(state.outrage.toFixed(3)),
490
+ trust: Number(state.trust.toFixed(3)),
491
+ amplification: Number(state.amplification.toFixed(3)),
492
+ cascadeRisk: Number(state.cascadeRisk.toFixed(3)),
493
+ avgImpact: Number(postMetrics.avgImpact.toFixed(3)),
494
+ agentCount: governed.length,
495
+ });
496
+ // --- Step 6: Determine trajectory ---
497
+ let trajectory;
498
+ if (state.coolingPeriod > 0) {
499
+ trajectory = "cooling";
500
+ }
501
+ else if (state.cascadeRisk > 0.7) {
502
+ trajectory = "critical";
503
+ }
504
+ else if (detectTrend(state.history, "outrage") === "rising" || detectTrend(state.history, "polarization") === "rising") {
505
+ trajectory = "escalating";
506
+ }
507
+ else if (detectTrend(state.history, "outrage") === "falling" && detectTrend(state.history, "polarization") === "falling") {
508
+ trajectory = "de-escalating";
509
+ }
510
+ else {
511
+ trajectory = "stable";
512
+ }
513
+ // Predict next-round risk
514
+ const predictedRisk = Math.min(1, state.cascadeRisk * 0.4 +
515
+ state.outrage * 0.3 +
516
+ state.polarization * 0.2 +
517
+ state.informationVelocity * 0.1);
518
+ return {
519
+ reactions: governed,
520
+ systemState: { ...state, history: [...state.history] },
521
+ interventions,
522
+ rulesFired,
523
+ trajectory,
524
+ predictedRisk: Number(predictedRisk.toFixed(3)),
525
+ };
526
+ }
527
+ function reset() {
528
+ state = {
529
+ polarization: 0.1,
530
+ outrage: 0.1,
531
+ trust: 0.7,
532
+ informationVelocity: 0.3,
533
+ amplification: 1.0,
534
+ cascadeRisk: 0.1,
535
+ contagionDepth: 2,
536
+ coolingPeriod: 0,
537
+ history: [],
538
+ ...initialState,
539
+ };
540
+ stats.totalRounds = 0;
541
+ stats.totalInterventions = 0;
542
+ stats.propagationLimits = 0;
543
+ stats.amplificationDampens = 0;
544
+ stats.cascadeBreakers = 0;
545
+ stats.coolingPeriods = 0;
546
+ stats.trustBoosts = 0;
547
+ stats.peakOutrage = 0;
548
+ stats.peakPolarization = 0;
549
+ stats.peakCascadeRisk = 0;
550
+ stats.trustSum = 0;
551
+ }
552
+ return {
553
+ governRound,
554
+ get state() { return { ...state, history: [...state.history] }; },
555
+ /** Mutate the internal state directly (used by adapters for governance feedback) */
556
+ mutateState(fn) { fn(state); },
557
+ get rules() { return [...dynamicsRules]; },
558
+ get stats() {
559
+ return {
560
+ totalRounds: stats.totalRounds,
561
+ totalInterventions: stats.totalInterventions,
562
+ propagationLimits: stats.propagationLimits,
563
+ amplificationDampens: stats.amplificationDampens,
564
+ cascadeBreakers: stats.cascadeBreakers,
565
+ coolingPeriods: stats.coolingPeriods,
566
+ trustBoosts: stats.trustBoosts,
567
+ peakOutrage: stats.peakOutrage,
568
+ peakPolarization: stats.peakPolarization,
569
+ peakCascadeRisk: stats.peakCascadeRisk,
570
+ averageTrust: stats.totalRounds > 0 ? Number((stats.trustSum / stats.totalRounds).toFixed(3)) : 0.7,
571
+ };
572
+ },
573
+ reset,
574
+ };
575
+ }
576
+ // ============================================
577
+ // CONVENIENCE: governDynamics() one-shot
578
+ // ============================================
579
+ /**
580
+ * One-shot dynamics governance for a single round.
581
+ *
582
+ * For repeated calls, use createDynamicsGovernor() instead.
583
+ * This is mainly useful for testing/playground.
584
+ */
585
+ function governDynamics(reactions, policyText, systemState) {
586
+ const governor = createDynamicsGovernor(policyText, systemState);
587
+ return governor.governRound(reactions, 0);
588
+ }