@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.
- package/README.md +562 -68
- package/dist/adapters/mirofish.js +461 -0
- package/dist/adapters/scienceclaw.js +750 -0
- package/dist/assets/index-B64NuIXu.css +1 -0
- package/dist/assets/index-DbzSnYxr.js +532 -0
- package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
- package/dist/assets/reportEngine-DKWTrP6-.js +1 -0
- package/dist/components/ConstraintsPanel.js +11 -0
- package/dist/components/StakeholderBuilder.js +32 -0
- package/dist/components/ui/badge.js +24 -0
- package/dist/components/ui/button.js +70 -0
- package/dist/components/ui/card.js +57 -0
- package/dist/components/ui/input.js +44 -0
- package/dist/components/ui/label.js +45 -0
- package/dist/components/ui/select.js +70 -0
- package/dist/engine/aiProvider.js +681 -0
- package/dist/engine/auditTrace.js +352 -0
- package/dist/engine/behavioralAnalysis.js +605 -0
- package/dist/engine/cli.js +1408 -299
- package/dist/engine/dynamicsGovernance.js +588 -0
- package/dist/engine/fullGovernedLoop.js +367 -0
- package/dist/engine/governance.js +8 -3
- package/dist/engine/governedSimulation.js +114 -17
- package/dist/engine/index.js +56 -1
- package/dist/engine/liveAdapter.js +342 -0
- package/dist/engine/liveVisualizer.js +4284 -0
- package/dist/engine/metrics/science.metrics.js +335 -0
- package/dist/engine/narrativeInjection.js +360 -0
- package/dist/engine/policyEnforcement.js +1611 -0
- package/dist/engine/policyEngine.js +799 -0
- package/dist/engine/primeRadiant.js +540 -0
- package/dist/engine/reasoningEngine.js +57 -3
- package/dist/engine/reportEngine.js +97 -0
- package/dist/engine/scenarioCapsule.js +56 -0
- package/dist/engine/scenarioComparison.js +463 -0
- package/dist/engine/scenarioLibrary.js +248 -0
- package/dist/engine/swarmSimulation.js +54 -1
- package/dist/engine/worldComparison.js +358 -0
- package/dist/engine/worldStorage.js +232 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +23 -0
- package/dist/lib/reasoningEngine.js +290 -0
- package/dist/lib/simulationAdapter.js +686 -0
- package/dist/lib/swarmParser.js +291 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils.js +8 -0
- package/dist/placeholder.svg +1 -0
- package/dist/robots.txt +14 -0
- package/dist/runtime/govern.js +473 -0
- package/dist/runtime/index.js +75 -0
- package/dist/runtime/types.js +11 -0
- package/package.json +17 -12
- package/variants/.gitkeep +0 -0
|
@@ -0,0 +1,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
|
+
}
|