@neuroverseos/nv-sim 0.1.2 → 0.1.5

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 (53) hide show
  1. package/README.md +562 -68
  2. package/dist/adapters/mirofish.js +461 -0
  3. package/dist/adapters/scienceclaw.js +750 -0
  4. package/dist/assets/index-B64NuIXu.css +1 -0
  5. package/dist/assets/index-DbzSnYxr.js +532 -0
  6. package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
  7. package/dist/assets/reportEngine-DKWTrP6-.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 +4284 -0
  27. package/dist/engine/metrics/science.metrics.js +335 -0
  28. package/dist/engine/narrativeInjection.js +360 -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/scenarioCapsule.js +56 -0
  35. package/dist/engine/scenarioComparison.js +463 -0
  36. package/dist/engine/scenarioLibrary.js +248 -0
  37. package/dist/engine/swarmSimulation.js +54 -1
  38. package/dist/engine/worldComparison.js +358 -0
  39. package/dist/engine/worldStorage.js +232 -0
  40. package/dist/favicon.ico +0 -0
  41. package/dist/index.html +23 -0
  42. package/dist/lib/reasoningEngine.js +290 -0
  43. package/dist/lib/simulationAdapter.js +686 -0
  44. package/dist/lib/swarmParser.js +291 -0
  45. package/dist/lib/types.js +2 -0
  46. package/dist/lib/utils.js +8 -0
  47. package/dist/placeholder.svg +1 -0
  48. package/dist/robots.txt +14 -0
  49. package/dist/runtime/govern.js +473 -0
  50. package/dist/runtime/index.js +75 -0
  51. package/dist/runtime/types.js +11 -0
  52. package/package.json +17 -12
  53. package/variants/.gitkeep +0 -0
@@ -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
+ }
@@ -330,6 +330,62 @@ exports.SCENARIO_TEMPLATES = {
330
330
  },
331
331
  },
332
332
  },
333
+ science_research: {
334
+ title: "Governed Science Research Pipeline",
335
+ scenario: "A research team uses AI agents to search literature, analyze findings, " +
336
+ "cross-reference sources, generate hypotheses, and publish results. " +
337
+ "Governance enforces scientific rigor: blocking unsupported claims, " +
338
+ "requiring source validation, and ensuring confidence thresholds before publication.",
339
+ stakeholders: [
340
+ { id: "Research Agent", disposition: "neutral", priorities: ["discovery", "accuracy", "publication"] },
341
+ { id: "Peer Reviewers", disposition: "neutral", priorities: ["rigor", "reproducibility", "novelty"] },
342
+ { id: "Journal Editors", disposition: "neutral", priorities: ["quality", "impact", "integrity"] },
343
+ { id: "Funding Bodies", disposition: "supportive", priorities: ["return on investment", "credibility", "public benefit"] },
344
+ { id: "Public", disposition: "neutral", priorities: ["trust", "safety", "accessibility"] },
345
+ { id: "Institutional Review", disposition: "neutral", priorities: ["ethics", "compliance", "methodology"] },
346
+ ],
347
+ assumptions: {
348
+ severity: "moderate",
349
+ environmental_hostility: "low",
350
+ time_pressure: "moderate",
351
+ regulatory_climate: "strict",
352
+ },
353
+ constraints: {
354
+ time_horizon: "research cycle",
355
+ regulatory: ["institutional review", "publication standards", "data integrity"],
356
+ risk_tolerance: "conservative",
357
+ },
358
+ depth: "full",
359
+ perspective: "strategic_advisor",
360
+ swarm: { enabled: true, rounds: 6, reaction_model: "rational" },
361
+ tags: ["science", "research", "ai-agents", "integrity", "scienceclaw"],
362
+ world: {
363
+ world_id: "science_research",
364
+ name: "Scientific Research Governance",
365
+ inline_definition: {
366
+ thesis: "AI-assisted research requires governance at every stage — from literature search through publication — to maintain scientific integrity and prevent unsupported claims",
367
+ state_variables: [
368
+ { id: "source_count", label: "Verified Sources", type: "number", range: { min: 0, max: 50 }, default_value: 0 },
369
+ { id: "confidence_level", label: "Confidence Level", type: "number", range: { min: 0, max: 1 }, default_value: 0.3 },
370
+ { id: "hypothesis_validated", label: "Hypothesis Validated", type: "boolean", default_value: false },
371
+ { id: "peer_review_status", label: "Peer Review Status", type: "enum", enum_values: ["none", "submitted", "reviewed", "approved"], default_value: "none" },
372
+ { id: "publication_readiness", label: "Publication Readiness %", type: "number", range: { min: 0, max: 100 }, default_value: 0 },
373
+ ],
374
+ invariants: [
375
+ { id: "INV-001", description: "Literature search must return at least 2 peer-reviewed sources before analysis can proceed", enforceable: true },
376
+ { id: "INV-002", description: "Claims must cite specific sources — unsupported assertions are blocked", enforceable: true },
377
+ { id: "INV-003", description: "Publication requires confidence level above 0.7 and validated hypothesis", enforceable: true },
378
+ { id: "INV-004", description: "Cross-referencing must compare at least 3 independent sources to confirm findings", enforceable: true },
379
+ { id: "INV-005", description: "Recommendations must include uncertainty language when confidence is below 0.9", enforceable: true },
380
+ ],
381
+ gates: [
382
+ { id: "GATE-001", label: "Insufficient Evidence", condition: "source_count < 2 && confidence_level > 0.5", severity: "critical" },
383
+ { id: "GATE-002", label: "Premature Publication", condition: "publication_readiness < 60 || !hypothesis_validated", severity: "critical" },
384
+ { id: "GATE-003", label: "Low Confidence Alert", condition: "confidence_level < 0.4", severity: "warning" },
385
+ ],
386
+ },
387
+ },
388
+ },
333
389
  };
334
390
  /**
335
391
  * Get a preset scenario as a full capsule.
@@ -0,0 +1,463 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-Scenario Comparison Runner — N Scenarios × M Policies
4
+ *
5
+ * The decision intelligence engine. This is MODE 3 of the Prime Radiant.
6
+ *
7
+ * Instead of:
8
+ * "Here's what happened"
9
+ * We produce:
10
+ * "Here's what you should do"
11
+ *
12
+ * Usage:
13
+ * const result = await runScenarioMatrix({
14
+ * scenario: "University disciplinary crisis",
15
+ * stakeholders: [...],
16
+ * paths: [...],
17
+ * policyOptions: [
18
+ * { id: "revoke", label: "Revoke Decision", policyText: "..." },
19
+ * { id: "partial", label: "Partial Apology", policyText: "..." },
20
+ * { id: "maintain", label: "Maintain Decision", policyText: "..." },
21
+ * ],
22
+ * evaluationMetrics: [
23
+ * { id: "trust", label: "Public Trust", weight: 0.3 },
24
+ * { id: "polarization", label: "Polarization Index", weight: 0.25, lowerIsBetter: true },
25
+ * ...
26
+ * ],
27
+ * })
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.DEFAULT_METRICS = void 0;
31
+ exports.runScenarioMatrix = runScenarioMatrix;
32
+ const fullGovernedLoop_1 = require("./fullGovernedLoop");
33
+ // ============================================
34
+ // DEFAULT METRICS
35
+ // ============================================
36
+ exports.DEFAULT_METRICS = [
37
+ {
38
+ id: "trust",
39
+ label: "Public Trust",
40
+ weight: 0.25,
41
+ extractor: (r) => r.finalState.trust,
42
+ },
43
+ {
44
+ id: "polarization",
45
+ label: "Polarization Index",
46
+ weight: 0.20,
47
+ lowerIsBetter: true,
48
+ extractor: (r) => r.finalState.polarization,
49
+ },
50
+ {
51
+ id: "outrage",
52
+ label: "Outrage Level",
53
+ weight: 0.15,
54
+ lowerIsBetter: true,
55
+ extractor: (r) => r.finalState.outrage,
56
+ },
57
+ {
58
+ id: "cascade_risk",
59
+ label: "Cascade Risk",
60
+ weight: 0.15,
61
+ lowerIsBetter: true,
62
+ extractor: (r) => r.finalState.cascadeRisk,
63
+ },
64
+ {
65
+ id: "system_health",
66
+ label: "System Health",
67
+ weight: 0.15,
68
+ extractor: (r) => r.systemHealthScore / 100,
69
+ },
70
+ {
71
+ id: "governance_effectiveness",
72
+ label: "Governance Effectiveness",
73
+ weight: 0.10,
74
+ extractor: (r) => {
75
+ const blocked = r.actionGovernance.blocked;
76
+ const total = r.actionGovernance.totalEvaluations;
77
+ const blockRate = total > 0 ? blocked / total : 0;
78
+ // Sweet spot: some blocking is good, too much is bad
79
+ if (blockRate < 0.1)
80
+ return 0.3; // too lenient
81
+ if (blockRate > 0.7)
82
+ return 0.2; // over-constrained
83
+ return 0.7 + (1 - Math.abs(blockRate - 0.3) * 3) * 0.3;
84
+ },
85
+ },
86
+ ];
87
+ // ============================================
88
+ // METRIC EXTRACTION
89
+ // ============================================
90
+ function extractMetric(metric, result) {
91
+ if (metric.extractor) {
92
+ return metric.extractor(result);
93
+ }
94
+ // Default extractors based on metric ID
95
+ switch (metric.id) {
96
+ case "trust": return result.finalState.trust;
97
+ case "polarization": return result.finalState.polarization;
98
+ case "outrage": return result.finalState.outrage;
99
+ case "cascade_risk": return result.finalState.cascadeRisk;
100
+ case "system_health": return result.systemHealthScore / 100;
101
+ case "amplification": return result.finalState.amplification;
102
+ default: return 0.5;
103
+ }
104
+ }
105
+ function normalizeScore(value, lowerIsBetter) {
106
+ if (lowerIsBetter) {
107
+ return Math.max(0, Math.min(1, 1 - value));
108
+ }
109
+ return Math.max(0, Math.min(1, value));
110
+ }
111
+ function rateScore(normalized) {
112
+ if (normalized >= 0.8)
113
+ return "excellent";
114
+ if (normalized >= 0.6)
115
+ return "good";
116
+ if (normalized >= 0.4)
117
+ return "moderate";
118
+ if (normalized >= 0.2)
119
+ return "poor";
120
+ return "critical";
121
+ }
122
+ function formatMetricValue(value, metricId) {
123
+ if (metricId === "system_health")
124
+ return `${(value * 100).toFixed(0)}/100`;
125
+ if (metricId.includes("risk") || metricId.includes("probability"))
126
+ return `${(value * 100).toFixed(0)}%`;
127
+ return value.toFixed(2);
128
+ }
129
+ // ============================================
130
+ // MAIN: runScenarioMatrix()
131
+ // ============================================
132
+ async function runScenarioMatrix(config) {
133
+ const metrics = config.evaluationMetrics.length > 0
134
+ ? config.evaluationMetrics
135
+ : exports.DEFAULT_METRICS;
136
+ const optionResults = [];
137
+ // --- Run simulation for each policy option ---
138
+ for (const option of config.policyOptions) {
139
+ config.onProgress?.(option.id, "simulating");
140
+ const worldDef = option.worldDef ?? config.defaultWorldDef;
141
+ const simulation = await (0, fullGovernedLoop_1.runFullGovernedSimulation)({
142
+ scenario: config.scenario,
143
+ stakeholders: config.stakeholders,
144
+ paths: config.paths,
145
+ swarmConfig: config.swarmConfig,
146
+ policyText: option.policyText,
147
+ worldDef,
148
+ narrativeEvents: config.narrativeEvents,
149
+ agentTypes: config.agentTypes,
150
+ });
151
+ config.onProgress?.(option.id, "evaluating");
152
+ // Score each metric
153
+ const metricScores = metrics.map((metric) => {
154
+ const value = extractMetric(metric, simulation);
155
+ const normalized = normalizeScore(value, metric.lowerIsBetter ?? false);
156
+ const weighted = normalized * metric.weight;
157
+ return {
158
+ metricId: metric.id,
159
+ metricLabel: metric.label,
160
+ value: Number(value.toFixed(3)),
161
+ normalizedScore: Number(normalized.toFixed(3)),
162
+ weightedScore: Number(weighted.toFixed(3)),
163
+ display: formatMetricValue(value, metric.id),
164
+ rating: rateScore(normalized),
165
+ };
166
+ });
167
+ const overallScore = metricScores.reduce((s, m) => s + m.weightedScore, 0);
168
+ // Extract outcomes
169
+ const outcomes = {
170
+ finalTrust: Number(simulation.finalState.trust.toFixed(3)),
171
+ finalPolarization: Number(simulation.finalState.polarization.toFixed(3)),
172
+ finalOutrage: Number(simulation.finalState.outrage.toFixed(3)),
173
+ finalCascadeRisk: Number(simulation.finalState.cascadeRisk.toFixed(3)),
174
+ peakOutrage: simulation.dynamicsGovernance.peakOutrage,
175
+ totalInterventions: simulation.dynamicsGovernance.totalInterventions,
176
+ actionsBlocked: simulation.actionGovernance.blocked,
177
+ systemHealth: simulation.systemHealthScore,
178
+ trajectory: simulation.trajectory,
179
+ cascadeBreakerFired: simulation.dynamicsGovernance.cascadeBreakers > 0,
180
+ coolingActivated: simulation.dynamicsGovernance.coolingPeriods > 0,
181
+ };
182
+ optionResults.push({
183
+ optionId: option.id,
184
+ optionLabel: option.label,
185
+ simulation,
186
+ metricScores,
187
+ overallScore: Number(overallScore.toFixed(3)),
188
+ outcomes,
189
+ });
190
+ }
191
+ // --- Build comparison matrix ---
192
+ const sortedResults = [...optionResults].sort((a, b) => b.overallScore - a.overallScore);
193
+ const comparison = buildComparisonMatrix(optionResults, metrics);
194
+ const rankings = buildRankings(sortedResults);
195
+ const riskAssessment = buildRiskAssessment(optionResults);
196
+ const recommendation = buildRecommendation(sortedResults, riskAssessment, config.policyOptions);
197
+ const decisionBrief = buildDecisionBrief(sortedResults, recommendation, riskAssessment);
198
+ return {
199
+ scenario: config.scenario,
200
+ optionResults,
201
+ comparison,
202
+ recommendation,
203
+ rankings,
204
+ riskAssessment,
205
+ decisionBrief,
206
+ };
207
+ }
208
+ // ============================================
209
+ // COMPARISON MATRIX BUILDER
210
+ // ============================================
211
+ function buildComparisonMatrix(results, metrics) {
212
+ const metricIds = metrics.map((m) => m.id);
213
+ // Find best/worst per metric
214
+ const bestPerMetric = {};
215
+ const worstPerMetric = {};
216
+ for (const metric of metrics) {
217
+ const values = results.map((r) => {
218
+ const score = r.metricScores.find((s) => s.metricId === metric.id);
219
+ return score?.normalizedScore ?? 0;
220
+ });
221
+ bestPerMetric[metric.id] = Math.max(...values);
222
+ worstPerMetric[metric.id] = Math.min(...values);
223
+ }
224
+ const sorted = [...results].sort((a, b) => b.overallScore - a.overallScore);
225
+ const rows = sorted.map((result, idx) => ({
226
+ optionId: result.optionId,
227
+ optionLabel: result.optionLabel,
228
+ values: metrics.map((metric) => {
229
+ const score = result.metricScores.find((s) => s.metricId === metric.id);
230
+ return {
231
+ metricId: metric.id,
232
+ value: score.value,
233
+ display: score.display,
234
+ isBest: score.normalizedScore === bestPerMetric[metric.id],
235
+ isWorst: score.normalizedScore === worstPerMetric[metric.id],
236
+ };
237
+ }),
238
+ overallScore: result.overallScore,
239
+ rank: idx + 1,
240
+ }));
241
+ return {
242
+ metrics: metricIds,
243
+ rows,
244
+ };
245
+ }
246
+ // ============================================
247
+ // RANKINGS
248
+ // ============================================
249
+ function buildRankings(sorted) {
250
+ return sorted.map((result, idx) => {
251
+ let verdict;
252
+ if (idx === 0)
253
+ verdict = "BEST";
254
+ else if (result.overallScore > 0.5)
255
+ verdict = "VIABLE";
256
+ else if (result.overallScore > 0.3)
257
+ verdict = "RISKY";
258
+ else
259
+ verdict = "AVOID";
260
+ const oneLiner = buildOneLiner(result, idx);
261
+ return {
262
+ rank: idx + 1,
263
+ optionId: result.optionId,
264
+ optionLabel: result.optionLabel,
265
+ overallScore: result.overallScore,
266
+ verdict,
267
+ oneLiner,
268
+ };
269
+ });
270
+ }
271
+ function buildOneLiner(result, rank) {
272
+ const outcomes = result.outcomes;
273
+ if (outcomes.cascadeBreakerFired) {
274
+ return "Required emergency circuit breaker — high risk scenario.";
275
+ }
276
+ if (outcomes.finalTrust > 0.6 && outcomes.finalPolarization < 0.3) {
277
+ return "Maintains trust and minimizes polarization.";
278
+ }
279
+ if (outcomes.finalOutrage > 0.6) {
280
+ return "Outrage remains elevated — risk of sustained negative dynamics.";
281
+ }
282
+ if (outcomes.finalCascadeRisk > 0.5) {
283
+ return "Cascade risk remains dangerous — structural instability.";
284
+ }
285
+ if (outcomes.systemHealth > 70) {
286
+ return "System stabilizes with good overall health.";
287
+ }
288
+ if (outcomes.totalInterventions > 10) {
289
+ return "Requires heavy governance intervention to maintain stability.";
290
+ }
291
+ return `Overall score: ${result.overallScore.toFixed(2)} — ${outcomes.trajectory} trajectory.`;
292
+ }
293
+ // ============================================
294
+ // RISK ASSESSMENT
295
+ // ============================================
296
+ function buildRiskAssessment(results) {
297
+ return results.map((result) => {
298
+ const risks = [];
299
+ const outcomes = result.outcomes;
300
+ if (outcomes.finalCascadeRisk > 0.5) {
301
+ risks.push({
302
+ risk: "Cascading failure — actions compound into system-wide instability",
303
+ probability: outcomes.finalCascadeRisk,
304
+ severity: outcomes.finalCascadeRisk > 0.7 ? "critical" : "high",
305
+ });
306
+ }
307
+ if (outcomes.finalPolarization > 0.5) {
308
+ risks.push({
309
+ risk: "Entrenched polarization — stakeholder base splits into opposing camps",
310
+ probability: outcomes.finalPolarization,
311
+ severity: outcomes.finalPolarization > 0.7 ? "high" : "moderate",
312
+ });
313
+ }
314
+ if (outcomes.finalTrust < 0.3) {
315
+ risks.push({
316
+ risk: "Institutional trust collapse — credibility cannot be recovered",
317
+ probability: 1 - outcomes.finalTrust,
318
+ severity: outcomes.finalTrust < 0.15 ? "critical" : "high",
319
+ });
320
+ }
321
+ if (outcomes.peakOutrage > 0.7) {
322
+ risks.push({
323
+ risk: "Outrage spike — may trigger media cascade or coordinated backlash",
324
+ probability: outcomes.peakOutrage,
325
+ severity: outcomes.peakOutrage > 0.85 ? "critical" : "high",
326
+ });
327
+ }
328
+ if (outcomes.actionsBlocked > outcomes.systemHealth * 0.3) {
329
+ risks.push({
330
+ risk: "Over-governance — too many blocked actions may freeze legitimate response",
331
+ probability: 0.5,
332
+ severity: "moderate",
333
+ });
334
+ }
335
+ let overallRisk;
336
+ const criticalRisks = risks.filter((r) => r.severity === "critical").length;
337
+ const highRisks = risks.filter((r) => r.severity === "high").length;
338
+ if (criticalRisks > 0)
339
+ overallRisk = "CRITICAL";
340
+ else if (highRisks > 1)
341
+ overallRisk = "HIGH";
342
+ else if (highRisks > 0 || risks.length > 2)
343
+ overallRisk = "MEDIUM";
344
+ else
345
+ overallRisk = "LOW";
346
+ return {
347
+ optionId: result.optionId,
348
+ optionLabel: result.optionLabel,
349
+ overallRisk,
350
+ risks,
351
+ };
352
+ });
353
+ }
354
+ // ============================================
355
+ // RECOMMENDATION ENGINE
356
+ // ============================================
357
+ function buildRecommendation(sorted, risks, options) {
358
+ const best = sorted[0];
359
+ const bestRisk = risks.find((r) => r.optionId === best.optionId);
360
+ const secondBest = sorted.length > 1 ? sorted[1] : null;
361
+ // Confidence: based on score gap and risk level
362
+ let confidence = 0.5;
363
+ if (secondBest) {
364
+ const gap = best.overallScore - secondBest.overallScore;
365
+ confidence += gap * 2; // bigger gap = more confidence
366
+ }
367
+ if (bestRisk?.overallRisk === "LOW")
368
+ confidence += 0.15;
369
+ else if (bestRisk?.overallRisk === "CRITICAL")
370
+ confidence -= 0.2;
371
+ confidence = Math.max(0.2, Math.min(0.95, confidence));
372
+ // Rationale
373
+ const parts = [];
374
+ parts.push(`"${best.optionLabel}" scores highest across the evaluation metrics (${best.overallScore.toFixed(2)}).`);
375
+ if (best.outcomes.finalTrust > 0.5) {
376
+ parts.push(`Preserves institutional trust at ${(best.outcomes.finalTrust * 100).toFixed(0)}%.`);
377
+ }
378
+ if (best.outcomes.finalPolarization < 0.3) {
379
+ parts.push(`Keeps polarization contained at ${(best.outcomes.finalPolarization * 100).toFixed(0)}%.`);
380
+ }
381
+ if (secondBest) {
382
+ parts.push(`Outperforms "${secondBest.optionLabel}" by ${((best.overallScore - secondBest.overallScore) * 100).toFixed(0)} points.`);
383
+ }
384
+ // Caveats
385
+ const caveats = [];
386
+ if (bestRisk?.overallRisk === "HIGH" || bestRisk?.overallRisk === "CRITICAL") {
387
+ caveats.push(`This option still carries ${bestRisk.overallRisk} risk — ${bestRisk.risks[0]?.risk}`);
388
+ }
389
+ if (best.outcomes.coolingActivated) {
390
+ caveats.push("Required a cooling period — expect delayed response dynamics.");
391
+ }
392
+ if (best.outcomes.cascadeBreakerFired) {
393
+ caveats.push("Circuit breaker was triggered — the system reached critical thresholds before stabilizing.");
394
+ }
395
+ if (confidence < 0.5) {
396
+ caveats.push("Score differences between options are small — the choice is not clear-cut.");
397
+ }
398
+ // Next steps
399
+ const nextSteps = [
400
+ `Implement the "${best.optionLabel}" policy as the primary approach.`,
401
+ ];
402
+ if (best.outcomes.totalInterventions > 5) {
403
+ nextSteps.push("Prepare governance mechanisms for active intervention — this option requires ongoing management.");
404
+ }
405
+ if (secondBest && secondBest.overallScore > best.overallScore - 0.1) {
406
+ nextSteps.push(`Keep "${secondBest.optionLabel}" as a fallback — score difference is marginal.`);
407
+ }
408
+ nextSteps.push("Monitor the system state metrics (trust, polarization, outrage) in real-time.");
409
+ return {
410
+ optionId: best.optionId,
411
+ optionLabel: best.optionLabel,
412
+ confidence: Number(confidence.toFixed(2)),
413
+ rationale: parts.join(" "),
414
+ caveats,
415
+ nextSteps,
416
+ };
417
+ }
418
+ // ============================================
419
+ // DECISION BRIEF
420
+ // ============================================
421
+ function buildDecisionBrief(sorted, recommendation, risks) {
422
+ const lines = [];
423
+ lines.push("DECISION INTELLIGENCE BRIEF");
424
+ lines.push("=".repeat(50));
425
+ lines.push("");
426
+ // Rankings table
427
+ lines.push("SCENARIO COMPARISON:");
428
+ lines.push("-".repeat(50));
429
+ const headers = ["Option", "Score", "Trust", "Polar.", "Risk", "Verdict"];
430
+ lines.push(` ${headers.map((h) => h.padEnd(12)).join("")}`);
431
+ lines.push(` ${"-".repeat(72)}`);
432
+ for (const result of sorted) {
433
+ const risk = risks.find((r) => r.optionId === result.optionId);
434
+ const rank = sorted.indexOf(result);
435
+ const verdict = rank === 0 ? "BEST" : result.overallScore > 0.5 ? "VIABLE" : result.overallScore > 0.3 ? "RISKY" : "AVOID";
436
+ const prefix = rank === 0 ? ">>> " : " ";
437
+ const row = [
438
+ result.optionLabel.slice(0, 11),
439
+ result.overallScore.toFixed(2),
440
+ `${(result.outcomes.finalTrust * 100).toFixed(0)}%`,
441
+ `${(result.outcomes.finalPolarization * 100).toFixed(0)}%`,
442
+ risk?.overallRisk ?? "?",
443
+ verdict,
444
+ ];
445
+ lines.push(`${prefix}${row.map((v) => v.padEnd(12)).join("")}`);
446
+ }
447
+ lines.push("");
448
+ lines.push("RECOMMENDATION:");
449
+ lines.push(` ${recommendation.rationale}`);
450
+ if (recommendation.caveats.length > 0) {
451
+ lines.push("");
452
+ lines.push("CAVEATS:");
453
+ for (const caveat of recommendation.caveats) {
454
+ lines.push(` - ${caveat}`);
455
+ }
456
+ }
457
+ lines.push("");
458
+ lines.push("NEXT STEPS:");
459
+ for (const step of recommendation.nextSteps) {
460
+ lines.push(` ${sorted.indexOf(sorted[0]) + 1}. ${step}`);
461
+ }
462
+ return lines.join("\n");
463
+ }