hoi4-agent-tools 2.2.0 → 2.3.3
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/CHANGELOG.md +28 -0
- package/README.md +30 -21
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
- package/dist/hoi4_agent_tools/core/engine.js +5 -0
- package/dist/hoi4_agent_tools/core/index.js +57 -1
- package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
- package/dist/hoi4_agent_tools/core/source/index.js +1 -0
- package/dist/hoi4_agent_tools/event/algorithms.js +178 -1
- package/dist/hoi4_agent_tools/event/graph.js +24 -9
- package/dist/hoi4_agent_tools/event/limits.js +5 -2
- package/dist/hoi4_agent_tools/event/service.js +182 -45
- package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +13 -63
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +4 -1
- package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
- package/dist/hoi4_agent_tools/probability/compare.js +178 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
- package/dist/hoi4_agent_tools/probability/index.js +13 -0
- package/dist/hoi4_agent_tools/probability/model.js +5 -0
- package/dist/hoi4_agent_tools/probability/rational.js +145 -0
- package/dist/hoi4_agent_tools/probability/render.js +307 -0
- package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
- package/dist/hoi4_agent_tools/probability/service.js +1168 -0
- package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
- package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
- package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
- package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
- package/dist/hoi4_agent_tools/runtime.js +109 -7
- package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
- package/dist/hoi4_agent_tools/technology/graph.js +30 -3
- package/dist/hoi4_agent_tools/technology/queries.js +70 -3
- package/dist/hoi4_agent_tools/technology/service.js +38 -2
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +2 -0
- package/docs/events.md +2 -2
- package/docs/probability.md +80 -0
- package/docs/research/probability-adapter-evidence.md +64 -0
- package/docs/setup.md +2 -0
- package/docs/technology.md +4 -2
- package/examples/clients/codex-config.toml +3 -0
- package/examples/clients/generic.json +8 -0
- package/examples/probability/adaptive-event-pool.json +92 -0
- package/examples/probability/adaptive-event-sequence-input.json +57 -0
- package/examples/probability/event-mtth-scenarios.json +57 -0
- package/examples/probability/event-mtth-simulate-input.json +37 -0
- package/examples/probability/focus-route-scenarios.json +61 -0
- package/examples/probability/focus-route-sweep-input.json +36 -0
- package/package.json +19 -8
- package/schemas/configuration.schema.json +747 -0
- package/schemas/continuous-focus-palette.schema.json +325 -0
- package/schemas/custom-weighted-pool.schema.json +298 -0
- package/schemas/event-feature-manifest.schema.json +63 -0
- package/schemas/focus-plan.schema.json +927 -0
- package/schemas/focus-planning-sidecar.schema.json +407 -0
- package/schemas/gui-animation-source.schema.json +176 -0
- package/schemas/gui-helper.schema.json +301 -0
- package/schemas/gui-scenario.schema.json +298 -0
- package/schemas/map-operation.schema.json +2284 -0
- package/schemas/operation-result.schema.json +327 -0
- package/schemas/probability-analysis-result.schema.json +2211 -0
- package/schemas/probability-compare-input.schema.json +664 -0
- package/schemas/probability-evaluate-input.schema.json +665 -0
- package/schemas/probability-inspect-input.schema.json +97 -0
- package/schemas/probability-render-input.schema.json +141 -0
- package/schemas/probability-scenario-set.schema.json +430 -0
- package/schemas/probability-sequence-input.schema.json +886 -0
- package/schemas/probability-simulate-input.schema.json +691 -0
- package/schemas/probability-sweep-input.schema.json +702 -0
- package/server.json +3 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
function gcd(left, right) {
|
|
2
|
+
let a = left < 0n ? -left : left;
|
|
3
|
+
let b = right < 0n ? -right : right;
|
|
4
|
+
while (b !== 0n)
|
|
5
|
+
[a, b] = [b, a % b];
|
|
6
|
+
return a === 0n ? 1n : a;
|
|
7
|
+
}
|
|
8
|
+
function powerOfTen(exponent) {
|
|
9
|
+
return 10n ** BigInt(exponent);
|
|
10
|
+
}
|
|
11
|
+
/** Exact rational arithmetic for Clausewitz decimal literals and modifier chains. */
|
|
12
|
+
export class Rational {
|
|
13
|
+
numerator;
|
|
14
|
+
denominator;
|
|
15
|
+
constructor(numerator, denominator = 1n) {
|
|
16
|
+
if (denominator === 0n)
|
|
17
|
+
throw new RangeError('Rational denominator cannot be zero');
|
|
18
|
+
const sign = denominator < 0n ? -1n : 1n;
|
|
19
|
+
const divisor = gcd(numerator, denominator);
|
|
20
|
+
this.numerator = (numerator / divisor) * sign;
|
|
21
|
+
this.denominator = (denominator / divisor) * sign;
|
|
22
|
+
}
|
|
23
|
+
static zero = new Rational(0n);
|
|
24
|
+
static one = new Rational(1n);
|
|
25
|
+
static parse(value) {
|
|
26
|
+
if (typeof value === 'bigint')
|
|
27
|
+
return new Rational(value);
|
|
28
|
+
const source = typeof value === 'number' ? String(value) : value.trim();
|
|
29
|
+
const match = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/u.exec(source);
|
|
30
|
+
if (match === null)
|
|
31
|
+
return undefined;
|
|
32
|
+
const digits = `${match[2] ?? '0'}${match[3] ?? ''}`;
|
|
33
|
+
const sign = match[1] === '-' ? -1n : 1n;
|
|
34
|
+
const exponent = Number(match[4] ?? '0') - (match[3]?.length ?? 0);
|
|
35
|
+
if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 10_000)
|
|
36
|
+
return undefined;
|
|
37
|
+
const integer = BigInt(digits) * sign;
|
|
38
|
+
return exponent >= 0
|
|
39
|
+
? new Rational(integer * powerOfTen(exponent))
|
|
40
|
+
: new Rational(integer, powerOfTen(-exponent));
|
|
41
|
+
}
|
|
42
|
+
add(other) {
|
|
43
|
+
return new Rational(this.numerator * other.denominator + other.numerator * this.denominator, this.denominator * other.denominator);
|
|
44
|
+
}
|
|
45
|
+
subtract(other) {
|
|
46
|
+
return this.add(other.negate());
|
|
47
|
+
}
|
|
48
|
+
multiply(other) {
|
|
49
|
+
return new Rational(this.numerator * other.numerator, this.denominator * other.denominator);
|
|
50
|
+
}
|
|
51
|
+
divide(other) {
|
|
52
|
+
if (other.numerator === 0n)
|
|
53
|
+
throw new RangeError('Cannot divide by zero');
|
|
54
|
+
return new Rational(this.numerator * other.denominator, this.denominator * other.numerator);
|
|
55
|
+
}
|
|
56
|
+
pow(exponent) {
|
|
57
|
+
if (!Number.isSafeInteger(exponent) || exponent < 0)
|
|
58
|
+
throw new RangeError('Rational exponent must be a non-negative safe integer');
|
|
59
|
+
return new Rational(this.numerator ** BigInt(exponent), this.denominator ** BigInt(exponent));
|
|
60
|
+
}
|
|
61
|
+
negate() {
|
|
62
|
+
return new Rational(-this.numerator, this.denominator);
|
|
63
|
+
}
|
|
64
|
+
compare(other) {
|
|
65
|
+
const difference = this.numerator * other.denominator - other.numerator * this.denominator;
|
|
66
|
+
return difference < 0n ? -1 : difference > 0n ? 1 : 0;
|
|
67
|
+
}
|
|
68
|
+
min(other) {
|
|
69
|
+
return this.compare(other) <= 0 ? this : other;
|
|
70
|
+
}
|
|
71
|
+
max(other) {
|
|
72
|
+
return this.compare(other) >= 0 ? this : other;
|
|
73
|
+
}
|
|
74
|
+
isZero() {
|
|
75
|
+
return this.numerator === 0n;
|
|
76
|
+
}
|
|
77
|
+
toNumber() {
|
|
78
|
+
return Number(this.numerator) / Number(this.denominator);
|
|
79
|
+
}
|
|
80
|
+
toDecimal(maximumPlaces = 18) {
|
|
81
|
+
const sign = this.numerator < 0n ? '-' : '';
|
|
82
|
+
const numerator = this.numerator < 0n ? -this.numerator : this.numerator;
|
|
83
|
+
const integer = numerator / this.denominator;
|
|
84
|
+
let remainder = numerator % this.denominator;
|
|
85
|
+
if (remainder === 0n)
|
|
86
|
+
return `${sign}${integer}`;
|
|
87
|
+
let decimals = '';
|
|
88
|
+
for (let index = 0; index < maximumPlaces && remainder !== 0n; index += 1) {
|
|
89
|
+
remainder *= 10n;
|
|
90
|
+
decimals += String(remainder / this.denominator);
|
|
91
|
+
remainder %= this.denominator;
|
|
92
|
+
}
|
|
93
|
+
return `${sign}${integer}.${decimals}`;
|
|
94
|
+
}
|
|
95
|
+
toJSON() {
|
|
96
|
+
return {
|
|
97
|
+
numerator: String(this.numerator),
|
|
98
|
+
denominator: String(this.denominator),
|
|
99
|
+
decimal: this.toDecimal(),
|
|
100
|
+
value: this.toNumber(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function sumRationals(values) {
|
|
105
|
+
return values.reduce((sum, value) => sum.add(value), Rational.zero);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Exact probability that each independent U(0, weight) score wins the maximum race.
|
|
109
|
+
* This is the selection rule documented for focus and technology AI scoring.
|
|
110
|
+
*/
|
|
111
|
+
export function uniformRaceProbabilities(weights) {
|
|
112
|
+
const positive = weights.map((weight) => weight.max(Rational.zero));
|
|
113
|
+
if (positive.every((weight) => weight.isZero()))
|
|
114
|
+
return weights.map(() => Rational.zero);
|
|
115
|
+
const ordered = positive
|
|
116
|
+
.filter((weight) => !weight.isZero())
|
|
117
|
+
.sort((left, right) => left.compare(right));
|
|
118
|
+
const groups = [];
|
|
119
|
+
for (const weight of ordered) {
|
|
120
|
+
const last = groups.at(-1);
|
|
121
|
+
if (last?.weight.compare(weight) === 0)
|
|
122
|
+
last.count += 1;
|
|
123
|
+
else
|
|
124
|
+
groups.push({ weight, count: 1 });
|
|
125
|
+
}
|
|
126
|
+
let activeProduct = ordered.reduce((product, weight) => product.multiply(weight), Rational.one);
|
|
127
|
+
let activeCount = ordered.length;
|
|
128
|
+
let low = Rational.zero;
|
|
129
|
+
let cumulative = Rational.zero;
|
|
130
|
+
const probabilityByWeight = new Map();
|
|
131
|
+
for (const group of groups) {
|
|
132
|
+
const contribution = group.weight
|
|
133
|
+
.pow(activeCount)
|
|
134
|
+
.subtract(low.pow(activeCount))
|
|
135
|
+
.divide(new Rational(BigInt(activeCount)))
|
|
136
|
+
.divide(activeProduct);
|
|
137
|
+
cumulative = cumulative.add(contribution);
|
|
138
|
+
probabilityByWeight.set(`${group.weight.numerator}/${group.weight.denominator}`, cumulative);
|
|
139
|
+
for (let index = 0; index < group.count; index += 1)
|
|
140
|
+
activeProduct = activeProduct.divide(group.weight);
|
|
141
|
+
activeCount -= group.count;
|
|
142
|
+
low = group.weight;
|
|
143
|
+
}
|
|
144
|
+
return positive.map((weight) => probabilityByWeight.get(`${weight.numerator}/${weight.denominator}`) ?? Rational.zero);
|
|
145
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { canonicalJson, compareCodeUnits } from '../core/canonical.js';
|
|
3
|
+
function escapeXml(value) {
|
|
4
|
+
return String(value)
|
|
5
|
+
.replaceAll('&', '&')
|
|
6
|
+
.replaceAll('<', '<')
|
|
7
|
+
.replaceAll('>', '>')
|
|
8
|
+
.replaceAll('"', '"')
|
|
9
|
+
.replaceAll("'", ''');
|
|
10
|
+
}
|
|
11
|
+
function valueFor(candidate) {
|
|
12
|
+
if (candidate.conditionalProbability !== undefined && candidate.conditionalProbability !== null)
|
|
13
|
+
return candidate.conditionalProbability;
|
|
14
|
+
if (candidate.conditionalProbabilityInterval !== undefined)
|
|
15
|
+
return candidate.conditionalProbabilityInterval.high;
|
|
16
|
+
if (candidate.cumulativeChance !== undefined)
|
|
17
|
+
return candidate.cumulativeChance;
|
|
18
|
+
if (candidate.cumulativeChanceInterval !== undefined)
|
|
19
|
+
return candidate.cumulativeChanceInterval.high;
|
|
20
|
+
if (candidate.effectiveMtthDays !== undefined)
|
|
21
|
+
return candidate.effectiveMtthDays;
|
|
22
|
+
if (typeof candidate.rawValue === 'object' && candidate.rawValue !== null)
|
|
23
|
+
return candidate.rawValue.value;
|
|
24
|
+
return candidate.rawInterval?.max ?? 0;
|
|
25
|
+
}
|
|
26
|
+
function frame(title, subtitle, width, height, content) {
|
|
27
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(title)}"><rect width="100%" height="100%" fill="#0d1721"/><text x="28" y="38" fill="#f1f5f8" font-family="Arial,sans-serif" font-size="22" font-weight="700">${escapeXml(title)}</text><text x="28" y="61" fill="#9fb3c8" font-family="Arial,sans-serif" font-size="12">${escapeXml(subtitle)}</text>${content}</svg>`;
|
|
28
|
+
}
|
|
29
|
+
function ranking(result) {
|
|
30
|
+
const scenario = result.scenarios[0];
|
|
31
|
+
const allRows = [...(scenario?.candidates ?? [])].sort((left, right) => valueFor(right) - valueFor(left) || compareCodeUnits(left.id, right.id));
|
|
32
|
+
const rows = allRows.slice(0, 100);
|
|
33
|
+
const maximum = Math.max(...rows.map(valueFor), 1e-12);
|
|
34
|
+
const width = 1_100;
|
|
35
|
+
const rowHeight = 28;
|
|
36
|
+
const height = Math.max(150, 95 + rows.length * rowHeight);
|
|
37
|
+
const content = rows
|
|
38
|
+
.map((candidate, index) => {
|
|
39
|
+
const value = valueFor(candidate);
|
|
40
|
+
const bar = (value / maximum) * 610;
|
|
41
|
+
const y = 86 + index * rowHeight;
|
|
42
|
+
const label = candidate.conditionalProbability === null || candidate.conditionalProbability === undefined
|
|
43
|
+
? value.toPrecision(6)
|
|
44
|
+
: `${(value * 100).toFixed(3)}%`;
|
|
45
|
+
return `<text x="28" y="${y + 16}" fill="#dce7ef" font-family="Arial,sans-serif" font-size="12">${escapeXml(candidate.id)}</text><rect x="360" y="${y}" width="${bar.toFixed(3)}" height="19" rx="3" fill="#4ca7d8"/><text x="${Math.min(1040, 370 + bar)}" y="${y + 15}" fill="#f1f5f8" font-family="Arial,sans-serif" font-size="11">${escapeXml(label)}</text>`;
|
|
46
|
+
})
|
|
47
|
+
.join('');
|
|
48
|
+
return frame(`Ranking — ${scenario?.id ?? 'no scenario'}`, `${result.adapter.id} · ${result.analysisId} · showing ${rows.length} of ${allRows.length}`, width, height, content);
|
|
49
|
+
}
|
|
50
|
+
function matrix(result) {
|
|
51
|
+
const candidateIds = [
|
|
52
|
+
...new Set(result.scenarios.flatMap(({ candidates }) => candidates.map(({ id }) => id))),
|
|
53
|
+
].slice(0, 100);
|
|
54
|
+
const scenarios = result.scenarios.slice(0, 250);
|
|
55
|
+
const cellWidth = Math.max(18, Math.min(80, 800 / Math.max(1, candidateIds.length)));
|
|
56
|
+
const cellHeight = 25;
|
|
57
|
+
const width = Math.max(700, 260 + candidateIds.length * cellWidth);
|
|
58
|
+
const height = Math.max(180, 115 + scenarios.length * cellHeight);
|
|
59
|
+
const usesProbability = scenarios.some(({ candidates }) => candidates.some(({ conditionalProbability }) => conditionalProbability !== undefined && conditionalProbability !== null));
|
|
60
|
+
const maximumRawValue = Math.max(...scenarios.flatMap(({ candidates }) => candidates.map(valueFor)), 1e-12);
|
|
61
|
+
const cells = scenarios
|
|
62
|
+
.flatMap((scenario, row) => candidateIds.map((candidateId, column) => {
|
|
63
|
+
const candidate = scenario.candidates.find(({ id }) => id === candidateId);
|
|
64
|
+
const value = candidate === undefined ? 0 : valueFor(candidate);
|
|
65
|
+
const normalized = Math.max(0, Math.min(1, usesProbability ? (candidate?.conditionalProbability ?? 0) : value / maximumRawValue));
|
|
66
|
+
const blue = Math.round(70 + normalized * 160);
|
|
67
|
+
return `<rect x="${250 + column * cellWidth}" y="${90 + row * cellHeight}" width="${Math.max(1, cellWidth - 1)}" height="${cellHeight - 1}" fill="rgb(30,${Math.round(75 + normalized * 90)},${blue})"><title>${escapeXml(`${scenario.id} / ${candidateId}: ${value}`)}</title></rect>`;
|
|
68
|
+
}))
|
|
69
|
+
.join('');
|
|
70
|
+
const labels = scenarios
|
|
71
|
+
.map(({ id }, row) => `<text x="242" y="${107 + row * cellHeight}" text-anchor="end" fill="#dce7ef" font-family="Arial,sans-serif" font-size="11">${escapeXml(id)}</text>`)
|
|
72
|
+
.join('');
|
|
73
|
+
const columns = candidateIds
|
|
74
|
+
.map((id, column) => `<text transform="translate(${263 + column * cellWidth},82) rotate(-45)" fill="#9fb3c8" font-family="Arial,sans-serif" font-size="9">${escapeXml(id)}</text>`)
|
|
75
|
+
.join('');
|
|
76
|
+
return frame('Scenario matrix', `${scenarios.length} scenarios · ${candidateIds.length} candidates`, width, height, `${cells}${labels}${columns}`);
|
|
77
|
+
}
|
|
78
|
+
function waterfall(result) {
|
|
79
|
+
const candidate = result.scenarios[0]?.candidates[0];
|
|
80
|
+
const trace = candidate?.trace ?? [];
|
|
81
|
+
const width = 1_200;
|
|
82
|
+
const height = Math.max(180, 110 + trace.length * 34);
|
|
83
|
+
const rows = trace
|
|
84
|
+
.map((step, index) => {
|
|
85
|
+
const y = 91 + index * 34;
|
|
86
|
+
const after = step.after?.decimal ?? 'bounded/unresolved';
|
|
87
|
+
return `<circle cx="45" cy="${y}" r="7" fill="${step.applied === 'true' ? '#56c596' : step.applied === 'false' ? '#667788' : '#f2b84b'}"/><line x1="52" y1="${y}" x2="82" y2="${y}" stroke="#526778"/><text x="92" y="${y + 4}" fill="#dce7ef" font-family="Arial,sans-serif" font-size="12">${escapeXml(`${step.operation}: ${step.expression}`)}</text><text x="860" y="${y + 4}" fill="#9fd6f2" font-family="Arial,sans-serif" font-size="12">${escapeXml(after)}</text>`;
|
|
88
|
+
})
|
|
89
|
+
.join('');
|
|
90
|
+
return frame(`Modifier trace — ${candidate?.id ?? 'no candidate'}`, result.scenarios[0]?.id ?? '', width, height, rows);
|
|
91
|
+
}
|
|
92
|
+
function timing(result) {
|
|
93
|
+
const rows = result.scenarios
|
|
94
|
+
.flatMap((scenario) => scenario.candidates
|
|
95
|
+
.filter(({ effectiveMtthDays }) => effectiveMtthDays !== undefined)
|
|
96
|
+
.map((candidate) => ({ scenario, candidate })))
|
|
97
|
+
.slice(0, 12);
|
|
98
|
+
const width = 1_200;
|
|
99
|
+
const height = 680;
|
|
100
|
+
const left = 90;
|
|
101
|
+
const top = 95;
|
|
102
|
+
const graphWidth = 820;
|
|
103
|
+
const graphHeight = 500;
|
|
104
|
+
const horizon = Math.max(...rows.map(({ scenario, candidate }) => Math.max(scenario.horizonDays ?? 0, candidate.timingQuantilesDays?.p95 ?? 0, (candidate.effectiveMtthDays ?? 1) * 4)), 1);
|
|
105
|
+
const colors = ['#7d69d8', '#4ca7d8', '#56c596', '#e2856e', '#d0a851', '#d879aa'];
|
|
106
|
+
const hazardAt = (row, day, bound) => {
|
|
107
|
+
const intervals = row.scenario.timingIntervals;
|
|
108
|
+
if (intervals === undefined || intervals.length === 0)
|
|
109
|
+
return day / Math.max(Number.MIN_VALUE, row.candidate.effectiveMtthDays ?? horizon);
|
|
110
|
+
return intervals.reduce((sum, interval) => {
|
|
111
|
+
const elapsed = Math.max(0, Math.min(day, interval.endDay) - interval.startDay);
|
|
112
|
+
const duration = interval.endDay - interval.startDay;
|
|
113
|
+
if (elapsed <= 0 || duration <= 0)
|
|
114
|
+
return sum;
|
|
115
|
+
const contribution = bound === 'minimum'
|
|
116
|
+
? interval.minimumHazardContribution
|
|
117
|
+
: interval.maximumHazardContribution;
|
|
118
|
+
return sum + contribution * (elapsed / duration);
|
|
119
|
+
}, 0);
|
|
120
|
+
};
|
|
121
|
+
const curves = rows
|
|
122
|
+
.map((row, index) => {
|
|
123
|
+
const color = colors[index % colors.length];
|
|
124
|
+
const points = Array.from({ length: 101 }, (_, point) => (horizon * point) / 100);
|
|
125
|
+
const path = (bound) => points
|
|
126
|
+
.map((day, point) => {
|
|
127
|
+
const survival = 2 ** -hazardAt(row, day, bound);
|
|
128
|
+
const x = left + (day / horizon) * graphWidth;
|
|
129
|
+
const y = top + (1 - survival) * graphHeight;
|
|
130
|
+
return `${point === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`;
|
|
131
|
+
})
|
|
132
|
+
.join(' ');
|
|
133
|
+
const label = `${row.scenario.id} / ${row.candidate.id}`;
|
|
134
|
+
return `<path d="${path('minimum')}" fill="none" stroke="${color}" stroke-width="2"><title>${escapeXml(`${label} upper survival`)}</title></path><path d="${path('maximum')}" fill="none" stroke="${color}" stroke-width="2" stroke-dasharray="5 4"><title>${escapeXml(`${label} lower survival`)}</title></path><rect x="940" y="${100 + index * 35}" width="16" height="4" fill="${color}"/><text x="966" y="${106 + index * 35}" fill="#dce7ef" font-family="Arial,sans-serif" font-size="10">${escapeXml(label)}</text>`;
|
|
135
|
+
})
|
|
136
|
+
.join('');
|
|
137
|
+
const axes = `<rect x="${left}" y="${top}" width="${graphWidth}" height="${graphHeight}" fill="#111f2a" stroke="#526778"/><text x="${left}" y="${top + graphHeight + 28}" fill="#9fb3c8" font-family="Arial,sans-serif" font-size="11">0 days</text><text x="${left + graphWidth}" y="${top + graphHeight + 28}" text-anchor="end" fill="#9fb3c8" font-family="Arial,sans-serif" font-size="11">${escapeXml(`${horizon.toFixed(1)} days`)}</text><text x="${left - 14}" y="${top + 4}" text-anchor="end" fill="#9fb3c8" font-family="Arial,sans-serif" font-size="11">100% survival</text><text x="${left - 14}" y="${top + graphHeight}" text-anchor="end" fill="#9fb3c8" font-family="Arial,sans-serif" font-size="11">0%</text>`;
|
|
138
|
+
return frame('MTTH survival curves', `${result.adapter.gameVersion} · solid upper survival, dashed lower survival`, width, height, `${axes}${curves}`);
|
|
139
|
+
}
|
|
140
|
+
function sensitivity(result) {
|
|
141
|
+
const points = result.sweep?.points ?? [];
|
|
142
|
+
const width = 1_100;
|
|
143
|
+
const height = 620;
|
|
144
|
+
const left = 80;
|
|
145
|
+
const top = 90;
|
|
146
|
+
const graphWidth = 940;
|
|
147
|
+
const graphHeight = 470;
|
|
148
|
+
const values = points.map(({ value }) => value);
|
|
149
|
+
const min = Math.min(...values, 0);
|
|
150
|
+
const max = Math.max(...values, 1);
|
|
151
|
+
const candidateIds = [
|
|
152
|
+
...new Set(points.flatMap(({ candidates }) => candidates.map(({ id }) => id))),
|
|
153
|
+
].slice(0, 12);
|
|
154
|
+
const plottedValues = points.flatMap(({ candidates }) => candidates.flatMap(({ conditionalProbability, rawValue }) => {
|
|
155
|
+
const value = conditionalProbability ?? rawValue;
|
|
156
|
+
return value === undefined ? [] : [value];
|
|
157
|
+
}));
|
|
158
|
+
const minimumY = Math.min(...plottedValues, 0);
|
|
159
|
+
const maximumY = Math.max(...plottedValues, 1e-12);
|
|
160
|
+
const ySpan = Math.max(1e-12, maximumY - minimumY);
|
|
161
|
+
const colors = [
|
|
162
|
+
'#4ca7d8',
|
|
163
|
+
'#e2856e',
|
|
164
|
+
'#71c78a',
|
|
165
|
+
'#d0a851',
|
|
166
|
+
'#9d82da',
|
|
167
|
+
'#63c5ba',
|
|
168
|
+
'#d879aa',
|
|
169
|
+
'#92a8c8',
|
|
170
|
+
];
|
|
171
|
+
const lines = candidateIds
|
|
172
|
+
.map((id, colorIndex) => {
|
|
173
|
+
const candidatePoints = points
|
|
174
|
+
.map((item) => ({
|
|
175
|
+
x: item.value,
|
|
176
|
+
y: item.candidates.find((candidate) => candidate.id === id)?.conditionalProbability ??
|
|
177
|
+
item.candidates.find((candidate) => candidate.id === id)?.rawValue ??
|
|
178
|
+
0,
|
|
179
|
+
}))
|
|
180
|
+
.sort((a, b) => a.x - b.x);
|
|
181
|
+
const path = candidatePoints
|
|
182
|
+
.map(({ x, y }, index) => `${index === 0 ? 'M' : 'L'} ${(left + ((x - min) / Math.max(1e-12, max - min)) * graphWidth).toFixed(2)} ${(top + graphHeight - ((y - minimumY) / ySpan) * graphHeight).toFixed(2)}`)
|
|
183
|
+
.join(' ');
|
|
184
|
+
return `<path d="${path}" fill="none" stroke="${colors[colorIndex % colors.length]}" stroke-width="2"><title>${escapeXml(id)}</title></path>`;
|
|
185
|
+
})
|
|
186
|
+
.join('');
|
|
187
|
+
return frame('Sensitivity sweep', `${points.length} evaluated points · y ${minimumY.toPrecision(4)}–${maximumY.toPrecision(4)}`, width, height, `<rect x="${left}" y="${top}" width="${graphWidth}" height="${graphHeight}" fill="#111f2a" stroke="#526778"/>${lines}`);
|
|
188
|
+
}
|
|
189
|
+
function thresholdMap(result) {
|
|
190
|
+
const reversals = result.sweep?.rankReversals ?? [];
|
|
191
|
+
const observations = result.diagnostics.filter(({ code }) => /(?:THRESHOLD|ACCEPTANCE_BAND|DOMINANT|STARVED|UNREACHABLE|UNEXPECTEDLY_COMMON)/u.test(code));
|
|
192
|
+
const rows = [
|
|
193
|
+
...reversals.map((reversal) => ({
|
|
194
|
+
label: `${reversal.scenarioId} / ${reversal.path}`,
|
|
195
|
+
detail: `${reversal.beforeLeader} -> ${reversal.afterLeader} between ${reversal.between[0]} and ${reversal.between[1]}`,
|
|
196
|
+
color: '#d0a851',
|
|
197
|
+
})),
|
|
198
|
+
...observations.map((diagnostic) => ({
|
|
199
|
+
label: diagnostic.code,
|
|
200
|
+
detail: diagnostic.message,
|
|
201
|
+
color: diagnostic.severity === 'error' || diagnostic.severity === 'blocker'
|
|
202
|
+
? '#e2856e'
|
|
203
|
+
: '#f2b84b',
|
|
204
|
+
})),
|
|
205
|
+
].slice(0, 500);
|
|
206
|
+
const width = 1_300;
|
|
207
|
+
const height = Math.max(180, 110 + rows.length * 34);
|
|
208
|
+
const content = rows
|
|
209
|
+
.map(({ label, detail, color }, index) => `<rect x="28" y="${82 + index * 34}" width="8" height="22" fill="${color}"/><text x="48" y="${98 + index * 34}" fill="#dce7ef" font-family="Arial,sans-serif" font-size="11">${escapeXml(label)}</text><text x="430" y="${98 + index * 34}" fill="#9fb3c8" font-family="Arial,sans-serif" font-size="11">${escapeXml(detail.slice(0, 135))}</text>`)
|
|
210
|
+
.join('');
|
|
211
|
+
return frame('Threshold map', `${reversals.length} rank reversals and ${observations.length} threshold observations`, width, height, content);
|
|
212
|
+
}
|
|
213
|
+
function sequence(result) {
|
|
214
|
+
const summary = result.sequence;
|
|
215
|
+
const rows = summary?.candidates ?? [];
|
|
216
|
+
const maximum = Math.max(...rows.map(({ expectedSelections }) => expectedSelections), 1e-12);
|
|
217
|
+
const width = 1_100;
|
|
218
|
+
const height = Math.max(180, 120 + rows.length * 36);
|
|
219
|
+
const content = rows
|
|
220
|
+
.map((candidate, index) => {
|
|
221
|
+
const y = 95 + index * 36;
|
|
222
|
+
const bar = (candidate.expectedSelections / maximum) * 600;
|
|
223
|
+
return `<text x="28" y="${y + 15}" fill="#dce7ef" font-family="Arial,sans-serif" font-size="12">${escapeXml(candidate.id)}</text><rect x="350" y="${y}" width="${bar.toFixed(3)}" height="20" fill="#56c596"/><text x="${Math.min(1010, 362 + bar)}" y="${y + 15}" fill="#f1f5f8" font-family="Arial,sans-serif" font-size="11">${escapeXml(`${candidate.expectedSelections.toFixed(4)} selections · ${(candidate.everSelectedProbability * 100).toFixed(2)}% ever`)}</text>`;
|
|
224
|
+
})
|
|
225
|
+
.join('');
|
|
226
|
+
return frame('Declared pool sequence', summary === undefined ? 'no sequence' : `${summary.method} · ${summary.steps} steps`, width, height, content);
|
|
227
|
+
}
|
|
228
|
+
function comparison(result) {
|
|
229
|
+
const rows = result.comparison?.scenarioChanges ?? [];
|
|
230
|
+
const width = 1_200;
|
|
231
|
+
const height = Math.max(180, 110 + rows.length * 31);
|
|
232
|
+
const content = rows
|
|
233
|
+
.slice(0, 250)
|
|
234
|
+
.map((change, index) => {
|
|
235
|
+
const y = 91 + index * 31;
|
|
236
|
+
const delta = change.probabilityDelta ?? change.rawDelta ?? 0;
|
|
237
|
+
const x = 700;
|
|
238
|
+
const bar = Math.min(350, Math.abs(delta) * 350);
|
|
239
|
+
return `<text x="28" y="${y + 13}" fill="#dce7ef" font-family="Arial,sans-serif" font-size="11">${escapeXml(`${change.scenarioId} / ${change.candidateId}`)}</text><line x1="${x}" y1="${y + 10}" x2="${x + (delta >= 0 ? bar : -bar)}" y2="${y + 10}" stroke="${delta >= 0 ? '#56c596' : '#e2856e'}" stroke-width="12"/><text x="1070" y="${y + 13}" fill="#f1f5f8" font-family="Arial,sans-serif" font-size="11">${escapeXml(delta.toPrecision(5))}</text>`;
|
|
240
|
+
})
|
|
241
|
+
.join('');
|
|
242
|
+
return frame('Before/after comparison', `${rows.length} attributed candidate changes`, width, height, content);
|
|
243
|
+
}
|
|
244
|
+
function unresolved(result) {
|
|
245
|
+
const rows = result.unresolved.slice(0, 250);
|
|
246
|
+
const width = 1_200;
|
|
247
|
+
const height = Math.max(180, 105 + rows.length * 34);
|
|
248
|
+
const content = rows
|
|
249
|
+
.map((item, index) => `<text x="28" y="${95 + index * 34}" fill="#f2b84b" font-family="Arial,sans-serif" font-size="11">${escapeXml(item.code)}</text><text x="280" y="${95 + index * 34}" fill="#dce7ef" font-family="Arial,sans-serif" font-size="11">${escapeXml(item.message.slice(0, 130))}</text>`)
|
|
250
|
+
.join('');
|
|
251
|
+
return frame('Unresolved analysis', `${result.unresolved.length} explicit uncertainties`, width, height, content);
|
|
252
|
+
}
|
|
253
|
+
function svgFor(result, visual) {
|
|
254
|
+
if (visual === 'ranking')
|
|
255
|
+
return ranking(result);
|
|
256
|
+
if (visual === 'matrix')
|
|
257
|
+
return matrix(result);
|
|
258
|
+
if (visual === 'waterfall')
|
|
259
|
+
return waterfall(result);
|
|
260
|
+
if (visual === 'timing')
|
|
261
|
+
return timing(result);
|
|
262
|
+
if (visual === 'sensitivity')
|
|
263
|
+
return sensitivity(result);
|
|
264
|
+
if (visual === 'threshold')
|
|
265
|
+
return thresholdMap(result);
|
|
266
|
+
if (visual === 'sequence')
|
|
267
|
+
return sequence(result);
|
|
268
|
+
if (visual === 'comparison')
|
|
269
|
+
return comparison(result);
|
|
270
|
+
return unresolved(result);
|
|
271
|
+
}
|
|
272
|
+
export async function renderProbabilityResult(result, visuals, includeHtml, provenance, signal) {
|
|
273
|
+
const writes = [];
|
|
274
|
+
for (const visual of [...new Set(visuals)]) {
|
|
275
|
+
signal?.throwIfAborted();
|
|
276
|
+
const svg = svgFor(result, visual);
|
|
277
|
+
const prefix = `probability-${result.analysisId}-${visual}`;
|
|
278
|
+
writes.push({
|
|
279
|
+
name: `${prefix}.svg`,
|
|
280
|
+
mimeType: 'image/svg+xml',
|
|
281
|
+
content: svg,
|
|
282
|
+
provenance,
|
|
283
|
+
description: `AI and MTTH ${visual} visualization`,
|
|
284
|
+
});
|
|
285
|
+
const png = await sharp(Buffer.from(svg, 'utf8'), { limitInputPixels: 268_402_689 })
|
|
286
|
+
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
287
|
+
.toBuffer();
|
|
288
|
+
writes.push({
|
|
289
|
+
name: `${prefix}.png`,
|
|
290
|
+
mimeType: 'image/png',
|
|
291
|
+
content: png,
|
|
292
|
+
provenance,
|
|
293
|
+
description: `AI and MTTH ${visual} raster visualization`,
|
|
294
|
+
});
|
|
295
|
+
if (includeHtml) {
|
|
296
|
+
const json = canonicalJson(result);
|
|
297
|
+
writes.push({
|
|
298
|
+
name: `${prefix}.html`,
|
|
299
|
+
mimeType: 'text/html',
|
|
300
|
+
content: `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeXml(visual)}</title><style>html,body{margin:0;background:#0d1721;color:#f1f5f8;font-family:system-ui,sans-serif}main{padding:16px}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><main>${svg}<details><summary>Authoritative JSON</summary><pre>${escapeXml(json)}</pre></details></main></body></html>`,
|
|
301
|
+
provenance,
|
|
302
|
+
description: `AI and MTTH ${visual} static report`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { writes, visualCount: visuals.length };
|
|
307
|
+
}
|