feat(forecast): add replayable deep forecast lifecycle (#2161)

* feat(forecast): add replayable deep forecast lifecycle

* fix(forecast): serialize replay snapshot market index
This commit is contained in:
Elie Habib
2026-03-23 23:59:21 +04:00
committed by GitHub
parent d294bde165
commit 2e0bc86d81
7 changed files with 1078 additions and 14 deletions

1
.gitignore vendored
View File

@@ -47,6 +47,7 @@ scripts/data/pizzint-partial.json
scripts/data/gpsjam-latest.json
scripts/data/mirta-raw.geojson
scripts/data/osm-military-raw.json
scripts/data/forecast-replays/
# Iran events data (sensitive, not for public repo)
scripts/data/iran-events-latest.json

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env node
import { loadEnvFile } from './_seed-utils.mjs';
import { readForecastTraceArtifactsForRun } from './seed-forecasts.mjs';
const _isDirectRun = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
if (_isDirectRun) loadEnvFile(import.meta.url);
function parseArgs(argv = []) {
const values = new Map();
for (const arg of argv) {
if (!arg.startsWith('--')) continue;
const [key, ...rest] = arg.slice(2).split('=');
values.set(key, rest.length > 0 ? rest.join('=') : 'true');
}
return {
baseline: values.get('baseline') || '',
candidate: values.get('candidate') || '',
};
}
function diffNumberMap(left = {}, right = {}) {
const keys = [...new Set([...Object.keys(left || {}), ...Object.keys(right || {})])].sort();
const diff = {};
for (const key of keys) {
diff[key] = Number((right?.[key] || 0) - (left?.[key] || 0));
}
return diff;
}
function extractStateLabels(artifacts = {}) {
const labels = Array.isArray(artifacts.snapshot?.fullRunStateUnits)
? artifacts.snapshot.fullRunStateUnits.map((item) => item?.label).filter(Boolean)
: Array.isArray(artifacts.worldState?.stateUnits)
? artifacts.worldState.stateUnits.map((item) => item?.label).filter(Boolean)
: [];
return [...new Set(labels)].sort();
}
function diffForecastRuns(baselineArtifacts = {}, candidateArtifacts = {}) {
const baselineSummary = baselineArtifacts.summary || {};
const candidateSummary = candidateArtifacts.summary || {};
const baselineTopTitles = new Set((baselineSummary.topForecasts || []).map((item) => item.title));
const candidateTopTitles = new Set((candidateSummary.topForecasts || []).map((item) => item.title));
const baselineLabels = new Set(extractStateLabels(baselineArtifacts));
const candidateLabels = new Set(extractStateLabels(candidateArtifacts));
return {
baselineRunId: baselineSummary.runId || '',
candidateRunId: candidateSummary.runId || '',
forecastDepth: {
baseline: baselineSummary.forecastDepth || '',
candidate: candidateSummary.forecastDepth || '',
},
deepForecastStatus: {
baseline: baselineSummary.deepForecast?.status || '',
candidate: candidateSummary.deepForecast?.status || '',
},
tracedForecastCountDelta: Number((candidateSummary.tracedForecastCount || 0) - (baselineSummary.tracedForecastCount || 0)),
impactExpansionDelta: {
candidateCount: Number((candidateSummary.worldStateSummary?.impactExpansionCandidateCount || 0) - (baselineSummary.worldStateSummary?.impactExpansionCandidateCount || 0)),
mappedSignalCount: Number((candidateSummary.worldStateSummary?.impactExpansionMappedSignalCount || 0) - (baselineSummary.worldStateSummary?.impactExpansionMappedSignalCount || 0)),
},
interactionDelta: {
simulation: Number((candidateSummary.worldStateSummary?.simulationInteractionCount || 0) - (baselineSummary.worldStateSummary?.simulationInteractionCount || 0)),
reportable: Number((candidateSummary.worldStateSummary?.reportableInteractionCount || 0) - (baselineSummary.worldStateSummary?.reportableInteractionCount || 0)),
},
publishedDomainDelta: diffNumberMap(
baselineSummary.quality?.traced?.domainCounts || {},
candidateSummary.quality?.traced?.domainCounts || {},
),
addedTopForecastTitles: [...candidateTopTitles].filter((title) => !baselineTopTitles.has(title)).sort(),
removedTopForecastTitles: [...baselineTopTitles].filter((title) => !candidateTopTitles.has(title)).sort(),
addedStateLabels: [...candidateLabels].filter((label) => !baselineLabels.has(label)).sort(),
removedStateLabels: [...baselineLabels].filter((label) => !candidateLabels.has(label)).sort(),
};
}
async function diffForecastRunIds({ baseline, candidate }) {
if (!baseline || !candidate) throw new Error('Missing --baseline or --candidate');
const [baselineArtifacts, candidateArtifacts] = await Promise.all([
readForecastTraceArtifactsForRun(baseline),
readForecastTraceArtifactsForRun(candidate),
]);
return diffForecastRuns(baselineArtifacts, candidateArtifacts);
}
if (_isDirectRun) {
const options = parseArgs(process.argv.slice(2));
const diff = await diffForecastRunIds(options);
console.log(JSON.stringify(diff, null, 2));
}
export {
parseArgs,
diffNumberMap,
diffForecastRuns,
diffForecastRunIds,
};

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env node
import { loadEnvFile } from './_seed-utils.mjs';
import {
findDuplicateStateUnitLabels,
readForecastTraceArtifactsForRun,
} from './seed-forecasts.mjs';
import { putR2JsonObject } from './_r2-storage.mjs';
const _isDirectRun = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
if (_isDirectRun) loadEnvFile(import.meta.url);
function parseArgs(argv = []) {
const values = new Map();
for (const arg of argv) {
if (!arg.startsWith('--')) continue;
const [key, ...rest] = arg.slice(2).split('=');
values.set(key, rest.length > 0 ? rest.join('=') : 'true');
}
return {
runId: values.get('run-id') || '',
};
}
function buildCheck(name, pass, severity = 'error', details = {}) {
return { name, pass, severity, ...details };
}
function hasHighValueDeepCandidate(snapshot = null) {
const candidates = Array.isArray(snapshot?.impactExpansionCandidates) ? snapshot.impactExpansionCandidates : [];
return candidates.some((packet) => {
const topBucket = String(packet?.marketContext?.topBucketId || '').toLowerCase();
const stateKind = String(packet?.stateKind || '').toLowerCase();
return Boolean(packet?.routeFacilityKey)
|| Boolean(packet?.commodityKey)
|| stateKind.includes('maritime')
|| stateKind.includes('transport')
|| ['energy', 'supply_chain', 'shipping', 'fx_stress', 'sovereign_risk'].includes(topBucket);
});
}
function evaluateForecastRunArtifacts(artifacts = {}) {
const summary = artifacts.summary || {};
const worldState = artifacts.worldState || {};
const runStatus = artifacts.runStatus || null;
const snapshot = artifacts.snapshot || null;
const fullRunStateUnits = Array.isArray(snapshot?.fullRunStateUnits) ? snapshot.fullRunStateUnits : [];
const selectedStateIds = Array.isArray(summary?.deepForecast?.selectedStateIds)
? summary.deepForecast.selectedStateIds
: Array.isArray(runStatus?.selectedDeepStateIds)
? runStatus.selectedDeepStateIds
: [];
const knownStateIds = new Set(fullRunStateUnits.map((unit) => unit?.id).filter(Boolean));
const unresolvedSelectedStateIds = selectedStateIds.filter((id) => !knownStateIds.has(id));
const duplicateLabels = findDuplicateStateUnitLabels(fullRunStateUnits);
const simulationInteractionCount = Number(worldState?.simulationState?.interactionLedger?.length || summary?.worldStateSummary?.simulationInteractionCount || 0);
const reportableInteractionCount = Number(worldState?.simulationState?.reportableInteractionLedger?.length || summary?.worldStateSummary?.reportableInteractionCount || 0);
const candidateSupplyChainCount = Number(summary?.quality?.candidateRun?.domainCounts?.supply_chain || 0);
const publishedSupplyChainCount = Number(summary?.quality?.traced?.domainCounts?.supply_chain || 0);
const mappedSignalCount = Number(worldState?.impactExpansion?.mappedSignalCount || summary?.worldStateSummary?.impactExpansionMappedSignalCount || 0);
const eligibleStateCount = Number(summary?.deepForecast?.eligibleStateCount || runStatus?.eligibleStateIds?.length || 0);
const checks = [
buildCheck('run_status_present', !!runStatus, 'error'),
buildCheck('deep_snapshot_present', !!snapshot, 'error'),
buildCheck('selected_state_ids_resolve', unresolvedSelectedStateIds.length === 0, 'error', {
unresolvedSelectedStateIds,
}),
buildCheck('duplicate_canonical_state_labels', duplicateLabels.length === 0, 'error', {
duplicateLabels,
}),
buildCheck('reportable_interactions_are_subset', reportableInteractionCount < simulationInteractionCount || simulationInteractionCount === 0, 'error', {
reportableInteractionCount,
simulationInteractionCount,
}),
buildCheck('supply_chain_survives_when_candidate_present', candidateSupplyChainCount === 0 || publishedSupplyChainCount > 0, 'warn', {
candidateSupplyChainCount,
publishedSupplyChainCount,
}),
buildCheck('eligible_high_value_deep_run_materializes_mapped_signals', eligibleStateCount === 0 || !hasHighValueDeepCandidate(snapshot) || mappedSignalCount > 0, 'error', {
eligibleStateCount,
mappedSignalCount,
}),
];
const failures = checks.filter((check) => !check.pass && check.severity === 'error');
const warnings = checks.filter((check) => !check.pass && check.severity !== 'error');
return {
runId: summary.runId || runStatus?.forecastRunId || '',
generatedAt: summary.generatedAt || artifacts.generatedAt || 0,
forecastDepth: summary.forecastDepth || worldState.forecastDepth || 'fast',
deepForecastStatus: summary.deepForecast?.status || runStatus?.status || '',
pass: failures.length === 0,
status: failures.length > 0 ? 'fail' : warnings.length > 0 ? 'warn' : 'pass',
failureCount: failures.length,
warningCount: warnings.length,
metrics: {
eligibleStateCount,
mappedSignalCount,
candidateSupplyChainCount,
publishedSupplyChainCount,
simulationInteractionCount,
reportableInteractionCount,
},
checks,
};
}
async function evaluateForecastRun({ runId }) {
if (!runId) throw new Error('Missing --run-id');
const artifacts = await readForecastTraceArtifactsForRun(runId);
const evaluation = evaluateForecastRunArtifacts(artifacts);
if (artifacts.storageConfig) {
await putR2JsonObject(artifacts.storageConfig, artifacts.keys.forecastEvalKey, evaluation, {
runid: String(runId || ''),
kind: 'forecast_eval',
});
}
return evaluation;
}
if (_isDirectRun) {
const options = parseArgs(process.argv.slice(2));
const evaluation = await evaluateForecastRun(options);
console.log(JSON.stringify(evaluation, null, 2));
if (!evaluation.pass) process.exit(1);
}
export {
parseArgs,
hasHighValueDeepCandidate,
evaluateForecastRunArtifacts,
evaluateForecastRun,
};

View File

@@ -6,8 +6,9 @@ import { runDeepForecastWorker } from './seed-forecasts.mjs';
loadEnvFile(import.meta.url);
const once = process.argv.includes('--once');
const runId = process.argv.find((arg) => arg.startsWith('--run-id='))?.split('=')[1] || '';
const result = await runDeepForecastWorker({ once });
const result = await runDeepForecastWorker({ once, runId });
if (once && result?.status && result.status !== 'idle') {
console.log(` [DeepForecast] ${result.status}`);
}

View File

@@ -0,0 +1,223 @@
#!/usr/bin/env node
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadEnvFile } from './_seed-utils.mjs';
import {
buildForecastTraceArtifacts,
evaluateDeepForecastPaths,
extractImpactExpansionBundle,
readForecastTraceArtifactsForRun,
} from './seed-forecasts.mjs';
import { getR2JsonObject } from './_r2-storage.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const _isDirectRun = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
if (_isDirectRun) loadEnvFile(import.meta.url);
function parseArgs(argv = []) {
const values = new Map();
for (const arg of argv) {
if (!arg.startsWith('--')) continue;
const [key, ...rest] = arg.slice(2).split('=');
values.set(key, rest.length > 0 ? rest.join('=') : 'true');
}
return {
runId: values.get('run-id') || '',
mode: values.get('mode') || 'deep',
providerMode: values.get('provider-mode') || 'recorded',
output: values.get('output') || '',
};
}
function buildEmptyImpactExpansionBundle(snapshot, failureReason) {
const candidatePackets = Array.isArray(snapshot?.impactExpansionCandidates) ? snapshot.impactExpansionCandidates : [];
return {
source: 'none',
provider: '',
model: '',
parseStage: '',
parseMode: '',
rawPreview: '',
failureReason,
candidateCount: candidatePackets.length,
extractedCandidateCount: 0,
extractedHypothesisCount: 0,
partialFailureCount: 0,
successfulCandidateCount: 0,
failedCandidatePreview: [],
candidates: candidatePackets.map((packet) => ({
candidateIndex: packet.candidateIndex,
candidateStateId: packet.candidateStateId,
label: packet.candidateStateLabel,
})),
candidatePackets,
extractedCandidates: [],
};
}
function buildReplayOutputPath(runId, mode, providerMode, explicitOutput = '') {
if (explicitOutput) return explicitOutput;
return join(__dirname, 'data', 'forecast-replays', `${runId}--${mode}--${providerMode}.json`);
}
function buildDeepReplayForecast(snapshot, evaluation) {
return {
...(snapshot?.deepForecast || {}),
status: evaluation?.status || 'completed_no_material_change',
completedAt: new Date().toISOString(),
selectedStateIds: (evaluation?.selectedPaths || [])
.filter((path) => path.type === 'expanded')
.map((path) => path.candidateStateId),
selectedPathCount: (evaluation?.selectedPaths || []).filter((path) => path.type === 'expanded').length,
replacedFastRun: evaluation?.status === 'completed',
rejectedPathsPreview: (evaluation?.rejectedPaths || []).slice(0, 6).map((path) => ({
pathId: path.pathId,
candidateStateId: path.candidateStateId,
acceptanceScore: Number(path.acceptanceScore || 0),
pathScore: Number(path.pathScore || 0),
})),
};
}
async function resolveReplayBundle({ providerMode, artifacts, snapshot, priorWorldState }) {
if (providerMode === 'recorded') {
return artifacts.impactExpansionDebug?.impactExpansionBundle
|| buildEmptyImpactExpansionBundle(snapshot, 'recorded_bundle_missing');
}
if (providerMode === 'none') {
return buildEmptyImpactExpansionBundle(snapshot, 'provider_mode_none');
}
return await extractImpactExpansionBundle({
candidatePackets: snapshot.impactExpansionCandidates || [],
priorWorldState,
});
}
async function replayForecastRun({
runId,
mode = 'deep',
providerMode = 'recorded',
output = '',
} = {}) {
if (!runId) throw new Error('Missing --run-id');
const artifacts = await readForecastTraceArtifactsForRun(runId);
if (!artifacts.snapshot) {
throw new Error(`Missing deep snapshot for run ${runId}`);
}
const snapshot = artifacts.snapshot;
const priorWorldState = snapshot.priorWorldStateKey
? await getR2JsonObject(artifacts.storageConfig, snapshot.priorWorldStateKey).catch(() => null)
: null;
let replayArtifacts;
let bundle = null;
let evaluation = null;
if (mode === 'fast') {
replayArtifacts = buildForecastTraceArtifacts({
...snapshot,
priorWorldState,
priorWorldStates: priorWorldState ? [priorWorldState] : [],
forecastDepth: 'fast',
deepForecast: snapshot.deepForecast || null,
runStatusContext: {
status: snapshot.deepForecast?.status || 'completed',
stage: 'fast_replay',
progressPercent: 100,
providerMode,
replaySourceRunId: runId,
},
}, {
runId: `${runId}-replay-fast`,
}, {
basePrefix: 'seed-data/forecast-replays',
});
} else {
bundle = await resolveReplayBundle({ providerMode, artifacts, snapshot, priorWorldState });
evaluation = await evaluateDeepForecastPaths(
snapshot,
priorWorldState,
snapshot.impactExpansionCandidates || [],
bundle,
);
const deepForecast = buildDeepReplayForecast(snapshot, evaluation);
replayArtifacts = buildForecastTraceArtifacts({
...snapshot,
priorWorldState,
priorWorldStates: priorWorldState ? [priorWorldState] : [],
impactExpansionBundle: evaluation.impactExpansionBundle || bundle,
deepPathEvaluation: evaluation,
forecastDepth: 'deep',
deepForecast,
worldStateOverride: evaluation.deepWorldState || undefined,
candidateWorldStateOverride: evaluation.deepWorldState || undefined,
runStatusContext: {
status: deepForecast.status,
stage: 'deep_replay',
progressPercent: 100,
processedCandidateCount: evaluation.impactExpansionBundle?.successfulCandidateCount || 0,
acceptedPathCount: deepForecast.selectedPathCount || 0,
completedAt: deepForecast.completedAt,
providerMode,
replaySourceRunId: runId,
},
}, {
runId: `${runId}-replay-deep`,
}, {
basePrefix: 'seed-data/forecast-replays',
});
}
const payload = {
sourceRunId: runId,
mode,
providerMode,
replayedAt: new Date().toISOString(),
snapshotKey: artifacts.snapshotKey,
bundleSummary: bundle ? {
source: bundle.source || '',
parseMode: bundle.parseMode || '',
parseStage: bundle.parseStage || '',
successfulCandidateCount: Number(bundle.successfulCandidateCount || 0),
extractedHypothesisCount: Number(bundle.extractedHypothesisCount || 0),
failureReason: bundle.failureReason || '',
} : null,
evaluationSummary: evaluation ? {
status: evaluation.status || '',
selectedPathCount: (evaluation.selectedPaths || []).length,
expandedPathCount: (evaluation.selectedPaths || []).filter((path) => path.type === 'expanded').length,
rejectedPathCount: (evaluation.rejectedPaths || []).length,
mappedSignalCount: Number(evaluation.deepWorldState?.impactExpansion?.mappedSignalCount || 0),
} : null,
artifacts: replayArtifacts,
};
const outputPath = buildReplayOutputPath(runId, mode, providerMode, output);
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
return {
outputPath,
payload,
};
}
if (_isDirectRun) {
const options = parseArgs(process.argv.slice(2));
const result = await replayForecastRun(options);
console.log(JSON.stringify({
runId: options.runId,
mode: options.mode,
providerMode: options.providerMode,
outputPath: result.outputPath,
evaluationSummary: result.payload.evaluationSummary,
}, null, 2));
}
export {
parseArgs,
buildEmptyImpactExpansionBundle,
replayForecastRun,
};

View File

@@ -4292,6 +4292,202 @@ function buildTraceRunPrefix(runId, generatedAt, basePrefix) {
return `${basePrefix}/${year}/${month}/${day}/${runId}`;
}
function parseForecastRunGeneratedAt(runId = '', fallback = Date.now()) {
const match = String(runId || '').match(/^(\d{10,})/);
if (!match) return fallback;
const parsed = Number(match[1]);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function buildForecastTraceArtifactKeys(runId, generatedAt, basePrefix) {
const prefix = buildTraceRunPrefix(runId, generatedAt, basePrefix);
return {
prefix,
manifestKey: `${prefix}/manifest.json`,
summaryKey: `${prefix}/summary.json`,
worldStateKey: `${prefix}/world-state.json`,
fastSummaryKey: `${prefix}/fast-summary.json`,
fastWorldStateKey: `${prefix}/fast-world-state.json`,
deepSummaryKey: `${prefix}/deep-summary.json`,
deepWorldStateKey: `${prefix}/deep-world-state.json`,
runStatusKey: `${prefix}/run-status.json`,
forecastEvalKey: `${prefix}/forecast-eval.json`,
impactExpansionDebugKey: `${prefix}/impact-expansion-debug.json`,
pathScorecardsKey: `${prefix}/path-scorecards.json`,
};
}
function buildForecastRunStatusPayload({
runId = '',
generatedAt = Date.now(),
forecastDepth = 'fast',
deepForecast = null,
worldState = null,
context = {},
} = {}) {
const mode = forecastDepth || worldState?.forecastDepth || 'fast';
const statusSource = context.status || deepForecast?.status || (mode === 'deep' ? 'running' : 'completed');
let stage = context.stage || '';
let progressPercent = Number.isFinite(context.progressPercent) ? context.progressPercent : null;
if (!stage) {
if (mode === 'fast') {
stage = statusSource === 'failed' ? 'fast_failed' : 'fast_published';
} else if (statusSource === 'running') {
stage = 'deep_running';
} else if (statusSource === 'failed') {
stage = 'deep_failed';
} else {
stage = 'deep_completed';
}
}
if (progressPercent == null) {
if (statusSource === 'running') progressPercent = 35;
else if (statusSource === 'queued') progressPercent = 0;
else progressPercent = 100;
}
const startedAt = context.startedAt
|| worldState?.deepForecast?.startedAt
|| deepForecast?.startedAt
|| new Date(generatedAt).toISOString();
const updatedAt = context.updatedAt || new Date().toISOString();
const completedAt = context.completedAt
|| deepForecast?.completedAt
|| (['completed', 'completed_no_material_change', 'failed', 'skipped'].includes(statusSource)
? new Date(generatedAt).toISOString()
: '');
return {
forecastRunId: runId,
mode,
status: statusSource,
stage,
progressPercent: Math.max(0, Math.min(100, Math.round(progressPercent))),
startedAt,
updatedAt,
completedAt,
eligibleStateIds: Array.isArray(deepForecast?.selectedStateIds) ? deepForecast.selectedStateIds : [],
processedCandidateCount: Number(context.processedCandidateCount ?? worldState?.impactExpansion?.successfulCandidateCount ?? 0),
acceptedPathCount: Number(context.acceptedPathCount ?? deepForecast?.selectedPathCount ?? 0),
failureReason: context.failureReason || deepForecast?.failureReason || worldState?.impactExpansion?.failureReason || '',
selectedDeepStateIds: Array.isArray(deepForecast?.selectedStateIds) ? deepForecast.selectedStateIds : [],
providerMode: context.providerMode || '',
replaySourceRunId: context.replaySourceRunId || '',
};
}
function summarizeImpactPathScore(path = null) {
if (!path) return null;
return {
pathId: path.pathId || '',
type: path.type || '',
candidateStateId: path.candidateStateId || '',
directVariableKey: path.direct?.variableKey || '',
secondVariableKey: path.second?.variableKey || '',
thirdVariableKey: path.third?.variableKey || '',
pathScore: Number(path.pathScore || 0),
acceptanceScore: Number(path.acceptanceScore || 0),
reportableQualityScore: Number(path.reportableQualityScore || 0),
marketCoherenceScore: Number(path.marketCoherenceScore || 0),
};
}
function buildDeepPathScorecardsPayload(data = {}, runId = '') {
const evaluation = data?.deepPathEvaluation || null;
if (!evaluation) return null;
return {
runId,
generatedAt: data?.generatedAt || Date.now(),
generatedAtIso: new Date(data?.generatedAt || Date.now()).toISOString(),
forecastDepth: data?.forecastDepth || 'fast',
status: evaluation.status || '',
selectedPaths: (evaluation.selectedPaths || []).map(summarizeImpactPathScore).filter(Boolean),
rejectedPaths: (evaluation.rejectedPaths || []).map(summarizeImpactPathScore).filter(Boolean),
};
}
function buildImpactExpansionDebugPayload(data = {}, worldState = null, runId = '') {
const bundle = data?.impactExpansionBundle || null;
const candidates = data?.impactExpansionCandidates || bundle?.candidatePackets || [];
if (!bundle && (!Array.isArray(candidates) || candidates.length === 0)) return null;
return {
runId,
generatedAt: data?.generatedAt || Date.now(),
generatedAtIso: new Date(data?.generatedAt || Date.now()).toISOString(),
forecastDepth: data?.forecastDepth || worldState?.forecastDepth || 'fast',
deepForecast: data?.deepForecast || worldState?.deepForecast || null,
impactExpansionBundle: bundle,
candidatePackets: candidates,
impactExpansionSummary: worldState?.impactExpansion || null,
selectedPaths: (data?.deepPathEvaluation?.selectedPaths || []).map(summarizeImpactPathScore).filter(Boolean),
rejectedPaths: (data?.deepPathEvaluation?.rejectedPaths || []).map(summarizeImpactPathScore).filter(Boolean),
};
}
async function writeForecastRunStatusArtifact({
runId = '',
generatedAt = Date.now(),
statusPayload = null,
storageConfig = null,
} = {}) {
if (!storageConfig || !runId || !statusPayload) return null;
const keys = buildForecastTraceArtifactKeys(runId, generatedAt, storageConfig.basePrefix || FORECAST_DEEP_RUN_PREFIX);
await putR2JsonObject(storageConfig, keys.runStatusKey, statusPayload, {
runid: String(runId || ''),
kind: 'run_status',
});
return keys.runStatusKey;
}
async function readForecastTraceArtifactsForRun(runId, options = {}) {
const storageConfig = options.storageConfig || resolveR2StorageConfig(options.env || process.env);
if (!storageConfig) throw new Error('R2 storage is not configured');
if (!runId) throw new Error('Missing runId');
const generatedAt = Number(options.generatedAt || parseForecastRunGeneratedAt(runId));
const keys = buildForecastTraceArtifactKeys(runId, generatedAt, storageConfig.basePrefix || FORECAST_DEEP_RUN_PREFIX);
const snapshotKey = buildDeepForecastSnapshotKey(runId, generatedAt, storageConfig.basePrefix || FORECAST_DEEP_RUN_PREFIX);
const [
manifest,
summary,
worldState,
fastSummary,
fastWorldState,
deepSummary,
deepWorldState,
runStatus,
impactExpansionDebug,
pathScorecards,
snapshot,
] = await Promise.all([
getR2JsonObject(storageConfig, keys.manifestKey),
getR2JsonObject(storageConfig, keys.summaryKey),
getR2JsonObject(storageConfig, keys.worldStateKey),
getR2JsonObject(storageConfig, keys.fastSummaryKey),
getR2JsonObject(storageConfig, keys.fastWorldStateKey),
getR2JsonObject(storageConfig, keys.deepSummaryKey),
getR2JsonObject(storageConfig, keys.deepWorldStateKey),
getR2JsonObject(storageConfig, keys.runStatusKey),
getR2JsonObject(storageConfig, keys.impactExpansionDebugKey),
getR2JsonObject(storageConfig, keys.pathScorecardsKey),
getR2JsonObject(storageConfig, snapshotKey),
]);
return {
storageConfig,
generatedAt,
keys,
snapshotKey,
manifest,
summary,
worldState,
fastSummary,
fastWorldState,
deepSummary,
deepWorldState,
runStatus,
impactExpansionDebug,
pathScorecards,
snapshot,
};
}
function buildForecastTraceRecord(pred, rank, simulationByForecastId = null) {
const caseFile = pred.caseFile || null;
let worldState = caseFile?.worldState || null;
@@ -9865,6 +10061,23 @@ function summarizeMarketInputCoverage(inputs = {}) {
return coverage;
}
function serializeSituationMarketContextIndex(index = null) {
if (!index || typeof index !== 'object') return null;
const bySituationId = index.bySituationId;
let serializedBySituationId = {};
if (bySituationId instanceof Map) {
serializedBySituationId = Object.fromEntries(bySituationId.entries());
} else if (Array.isArray(bySituationId)) {
serializedBySituationId = Object.fromEntries(bySituationId);
} else if (bySituationId && typeof bySituationId === 'object') {
serializedBySituationId = bySituationId;
}
return {
...index,
bySituationId: serializedBySituationId,
};
}
function flattenImpactExpansionHypotheses(bundle = null) {
const candidatePackets = Array.isArray(bundle?.candidatePackets) ? bundle.candidatePackets : [];
const extractedCandidates = Array.isArray(bundle?.extractedCandidates) ? bundle.extractedCandidates : [];
@@ -10697,6 +10910,35 @@ function annotateDeepForecastOrigins(worldState, acceptedPaths = []) {
return worldState;
}
function findDuplicateStateUnitLabels(stateUnits = []) {
const counts = new Map();
for (const unit of stateUnits || []) {
const label = String(unit?.label || '')
.replace(/\s+/g, ' ')
.trim();
if (!label) continue;
counts.set(label, (counts.get(label) || 0) + 1);
}
return [...counts.entries()]
.filter(([, count]) => count > 1)
.map(([label, count]) => ({ label, count }));
}
function validateDeepForecastSnapshot(snapshot = {}) {
const fullRunStateUnits = Array.isArray(snapshot?.fullRunStateUnits) ? snapshot.fullRunStateUnits : [];
const stateIds = new Set(fullRunStateUnits.map((unit) => unit?.id).filter(Boolean));
const selectedStateIds = Array.isArray(snapshot?.deepForecast?.selectedStateIds)
? snapshot.deepForecast.selectedStateIds.filter(Boolean)
: [];
const unresolvedSelectedStateIds = selectedStateIds.filter((id) => !stateIds.has(id));
const duplicateStateLabels = findDuplicateStateUnitLabels(fullRunStateUnits);
return {
pass: unresolvedSelectedStateIds.length === 0 && duplicateStateLabels.length === 0,
unresolvedSelectedStateIds,
duplicateStateLabels,
};
}
function buildDeepWorldStateFromSnapshot(snapshot, priorWorldState, impactExpansionBundle, deepForecastMeta = {}) {
return buildForecastRunWorldState({
generatedAt: snapshot.generatedAt,
@@ -10963,14 +11205,25 @@ function buildForecastTraceArtifacts(data, context = {}, config = {}) {
impactExpansionBundle: data?.impactExpansionBundle || null,
})
: null);
const prefix = buildTraceRunPrefix(
const artifactKeys = buildForecastTraceArtifactKeys(
context.runId || `run_${generatedAt}`,
generatedAt,
config.basePrefix || 'seed-data/forecast-traces'
config.basePrefix || 'seed-data/forecast-traces',
);
const manifestKey = `${prefix}/manifest.json`;
const summaryKey = `${prefix}/summary.json`;
const worldStateKey = `${prefix}/world-state.json`;
const {
prefix,
manifestKey,
summaryKey,
worldStateKey,
fastSummaryKey,
fastWorldStateKey,
deepSummaryKey,
deepWorldStateKey,
runStatusKey,
forecastEvalKey,
impactExpansionDebugKey,
pathScorecardsKey,
} = artifactKeys;
const forecastKeys = tracedPredictions.map(item => ({
id: item.id,
title: item.title,
@@ -10989,6 +11242,14 @@ function buildForecastTraceArtifacts(data, context = {}, config = {}) {
manifestKey,
summaryKey,
worldStateKey,
fastSummaryKey,
fastWorldStateKey,
deepSummaryKey,
deepWorldStateKey,
runStatusKey,
forecastEvalKey: artifactKeys.forecastEvalKey,
impactExpansionDebugKey,
pathScorecardsKey,
forecastKeys,
};
@@ -11099,6 +11360,21 @@ function buildForecastTraceArtifacts(data, context = {}, config = {}) {
})),
};
const runStatus = buildForecastRunStatusPayload({
runId: manifest.runId,
generatedAt: manifest.generatedAt,
forecastDepth: worldState.forecastDepth || data?.forecastDepth || 'fast',
deepForecast: worldState.deepForecast || data?.deepForecast || null,
worldState,
context: data?.runStatusContext || {},
});
const impactExpansionDebug = buildImpactExpansionDebugPayload(
data,
worldState,
manifest.runId,
);
const pathScorecards = buildDeepPathScorecardsPayload(data, manifest.runId);
return {
prefix,
manifestKey,
@@ -11107,6 +11383,17 @@ function buildForecastTraceArtifacts(data, context = {}, config = {}) {
summary,
worldStateKey,
worldState,
fastSummaryKey,
fastWorldStateKey,
deepSummaryKey,
deepWorldStateKey,
runStatusKey,
forecastEvalKey: artifactKeys.forecastEvalKey,
runStatus,
impactExpansionDebugKey,
impactExpansionDebug,
pathScorecardsKey,
pathScorecards,
forecasts: tracedPredictions.map(item => ({
key: `${prefix}/forecasts/${item.id}.json`,
payload: item,
@@ -11211,6 +11498,41 @@ async function writeForecastTraceArtifacts(data, context = {}) {
runid: String(artifacts.manifest.runId || ''),
kind: 'world_state',
});
if ((artifacts.summary.forecastDepth || 'fast') === 'deep') {
await putR2JsonObject(storageConfig, artifacts.deepSummaryKey, artifacts.summary, {
runid: String(artifacts.manifest.runId || ''),
kind: 'deep_summary',
});
await putR2JsonObject(storageConfig, artifacts.deepWorldStateKey, artifacts.worldState, {
runid: String(artifacts.manifest.runId || ''),
kind: 'deep_world_state',
});
} else {
await putR2JsonObject(storageConfig, artifacts.fastSummaryKey, artifacts.summary, {
runid: String(artifacts.manifest.runId || ''),
kind: 'fast_summary',
});
await putR2JsonObject(storageConfig, artifacts.fastWorldStateKey, artifacts.worldState, {
runid: String(artifacts.manifest.runId || ''),
kind: 'fast_world_state',
});
}
await putR2JsonObject(storageConfig, artifacts.runStatusKey, artifacts.runStatus, {
runid: String(artifacts.manifest.runId || ''),
kind: 'run_status',
});
if (artifacts.impactExpansionDebug) {
await putR2JsonObject(storageConfig, artifacts.impactExpansionDebugKey, artifacts.impactExpansionDebug, {
runid: String(artifacts.manifest.runId || ''),
kind: 'impact_expansion_debug',
});
}
if (artifacts.pathScorecards) {
await putR2JsonObject(storageConfig, artifacts.pathScorecardsKey, artifacts.pathScorecards, {
runid: String(artifacts.manifest.runId || ''),
kind: 'path_scorecards',
});
}
await Promise.all(
artifacts.forecasts.map((item, index) => putR2JsonObject(storageConfig, item.key, item.payload, {
runid: String(artifacts.manifest.runId || ''),
@@ -11230,6 +11552,7 @@ async function writeForecastTraceArtifacts(data, context = {}) {
manifestKey: artifacts.manifestKey,
summaryKey: artifacts.summaryKey,
worldStateKey: artifacts.worldStateKey,
runStatusKey: artifacts.runStatusKey,
forecastCount: artifacts.manifest.forecastCount,
tracedForecastCount: artifacts.manifest.tracedForecastCount,
triggerContext: artifacts.manifest.triggerContext,
@@ -11349,9 +11672,15 @@ function buildDeepForecastSnapshotPayload(data = {}, context = {}) {
fullRunSituationClusters: data.fullRunSituationClusters || [],
fullRunSituationFamilies: data.fullRunSituationFamilies || [],
fullRunStateUnits: data.fullRunStateUnits || [],
selectionWorldSignals: data.selectionWorldSignals || null,
selectionMarketTransmission: data.selectionMarketTransmission || null,
selectionMarketState: data.selectionMarketState || null,
selectionMarketInputCoverage: data.selectionMarketInputCoverage || null,
marketSelectionIndex: serializeSituationMarketContextIndex(data.marketSelectionIndex),
triggerContext: data.triggerContext || null,
enrichmentMeta: data.enrichmentMeta || null,
publishTelemetry: data.publishTelemetry || null,
forecastDepth: data.forecastDepth || 'fast',
deepForecast: data.deepForecast || null,
impactExpansionCandidates: data.impactExpansionCandidates || [],
priorWorldStateKey: data.priorWorldStateKey || '',
@@ -13402,6 +13731,11 @@ async function fetchForecasts() {
fullRunSituationClusters,
fullRunSituationFamilies,
fullRunStateUnits,
selectionWorldSignals,
selectionMarketTransmission,
selectionMarketState,
selectionMarketInputCoverage,
marketSelectionIndex,
impactExpansionCandidates,
deepForecast,
priorWorldStateKey: priorTracePointer?.worldStateKey || '',
@@ -13491,6 +13825,33 @@ async function processDeepForecastTask(task = {}) {
if (!storageConfig) return { status: 'skipped', reason: 'storage_not_configured' };
const snapshot = await getR2JsonObject(storageConfig, task.snapshotKey);
if (!snapshot?.runId) return { status: 'skipped', reason: 'missing_snapshot' };
const snapshotValidation = validateDeepForecastSnapshot(snapshot);
if (!snapshotValidation.pass) {
const errors = [];
if (snapshotValidation.unresolvedSelectedStateIds.length > 0) {
errors.push(`unresolved_selected_state_ids:${snapshotValidation.unresolvedSelectedStateIds.join(',')}`);
}
if (snapshotValidation.duplicateStateLabels.length > 0) {
errors.push(`duplicate_canonical_state_labels:${snapshotValidation.duplicateStateLabels.map((item) => item.label).join(',')}`);
}
throw new Error(errors.join(';'));
}
await writeForecastRunStatusArtifact({
runId: snapshot.runId,
generatedAt: snapshot.generatedAt,
storageConfig,
statusPayload: buildForecastRunStatusPayload({
runId: snapshot.runId,
generatedAt: snapshot.generatedAt,
forecastDepth: 'deep',
deepForecast: snapshot.deepForecast || null,
context: {
status: 'running',
stage: 'deep_running',
progressPercent: 15,
},
}),
});
const priorWorldState = task.priorWorldStateKey
? await getR2JsonObject(storageConfig, task.priorWorldStateKey).catch(() => null)
: null;
@@ -13521,6 +13882,7 @@ async function processDeepForecastTask(task = {}) {
priorWorldState,
priorWorldStates: priorWorldState ? [priorWorldState] : [],
impactExpansionBundle: evaluation.impactExpansionBundle || null,
deepPathEvaluation: evaluation,
};
if (evaluation.status === 'completed') {
@@ -13535,6 +13897,14 @@ async function processDeepForecastTask(task = {}) {
deepForecast,
worldStateOverride: evaluation.deepWorldState,
candidateWorldStateOverride: evaluation.deepWorldState,
runStatusContext: {
status: 'completed',
stage: 'deep_completed',
progressPercent: 100,
processedCandidateCount: evaluation.impactExpansionBundle?.successfulCandidateCount || 0,
acceptedPathCount: deepForecast.selectedPathCount || 0,
completedAt: deepForecast.completedAt,
},
}, { runId: snapshot.runId });
return { status: 'completed', deepForecast };
}
@@ -13548,6 +13918,14 @@ async function processDeepForecastTask(task = {}) {
...dataForWrite,
forecastDepth: 'deep',
deepForecast,
runStatusContext: {
status: deepForecast.status,
stage: 'deep_completed',
progressPercent: 100,
processedCandidateCount: evaluation.impactExpansionBundle?.successfulCandidateCount || 0,
acceptedPathCount: deepForecast.selectedPathCount || 0,
completedAt: deepForecast.completedAt,
},
}, { runId: snapshot.runId });
return { status: deepForecast.status, deepForecast };
}
@@ -13570,12 +13948,19 @@ async function writeFailedDeepForecastArtifacts(task = {}, failureReason = '') {
...snapshot,
forecastDepth: 'fast',
deepForecast,
runStatusContext: {
status: 'failed',
stage: 'deep_failed',
progressPercent: 100,
failureReason: deepForecast.failureReason,
completedAt: deepForecast.completedAt,
},
}, { runId: snapshot.runId });
}
async function processNextDeepForecastTask(options = {}) {
const workerId = options.workerId || `worker-${process.pid}-${Date.now()}`;
const queuedRunIds = await listQueuedDeepForecastTasks(10);
const queuedRunIds = options.runId ? [options.runId] : await listQueuedDeepForecastTasks(10);
for (const runId of queuedRunIds) {
const task = await claimDeepForecastTask(runId, workerId);
if (!task) continue;
@@ -13595,9 +13980,9 @@ async function processNextDeepForecastTask(options = {}) {
return { status: 'idle' };
}
async function runDeepForecastWorker({ once = false } = {}) {
async function runDeepForecastWorker({ once = false, runId = '' } = {}) {
for (;;) {
const result = await processNextDeepForecastTask();
const result = await processNextDeepForecastTask({ runId });
if (once) return result;
if (result?.status === 'idle') {
await sleep(FORECAST_DEEP_POLL_INTERVAL_MS);
@@ -13645,12 +14030,13 @@ if (_isDirectRun) {
replacedFastRun: false,
rejectedPathsPreview: [],
};
const snapshotPayload = buildDeepForecastSnapshotPayload({
...data,
triggerContext,
forecastDepth: 'fast',
}, { runId });
const snapshotWrite = await writeDeepForecastSnapshot(snapshotPayload, { runId });
if (deepForecast.status === 'queued' && (data.impactExpansionCandidates || []).length > 0) {
const snapshotPayload = buildDeepForecastSnapshotPayload({
...data,
triggerContext,
}, { runId });
const snapshotWrite = await writeDeepForecastSnapshot(snapshotPayload, { runId });
if (snapshotWrite?.snapshotKey) {
const queueResult = await enqueueDeepForecastTask({
runId,
@@ -13675,6 +14061,8 @@ if (_isDirectRun) {
failureReason: 'snapshot_write_failed',
};
}
} else if (!snapshotWrite?.snapshotKey) {
console.warn(' [DeepForecast] Snapshot write skipped or failed; replay will not be available for this run');
}
console.log(' [Trace] Starting R2 export...');
const pointer = await writeForecastTraceArtifacts({
@@ -13682,6 +14070,13 @@ if (_isDirectRun) {
triggerContext,
forecastDepth: 'fast',
deepForecast,
runStatusContext: {
status: deepForecast.status,
stage: 'fast_published',
progressPercent: 100,
completedAt: deepForecast.completedAt || '',
failureReason: deepForecast.failureReason || '',
},
}, { runId });
if (pointer) {
console.log(` [Trace] Written: ${pointer.summaryKey} (${pointer.tracedForecastCount} forecasts)`);
@@ -13751,6 +14146,11 @@ export {
buildForecastTraceRecord,
buildForecastTraceArtifacts,
writeForecastTraceArtifacts,
buildForecastTraceArtifactKeys,
parseForecastRunGeneratedAt,
readForecastTraceArtifactsForRun,
buildForecastRunStatusPayload,
writeForecastRunStatusArtifact,
buildChangeItems,
buildChangeSummary,
annotateForecastChanges,
@@ -13828,8 +14228,12 @@ export {
computeDeepMarketCoherenceScore,
computeDeepPathAcceptanceScore,
evaluateDeepForecastPaths,
findDuplicateStateUnitLabels,
validateDeepForecastSnapshot,
validateImpactHypotheses,
materializeImpactExpansion,
serializeSituationMarketContextIndex,
buildDeepForecastSnapshotKey,
buildDeepForecastSnapshotPayload,
writeDeepForecastSnapshot,
enqueueDeepForecastTask,

View File

@@ -6,6 +6,7 @@ import {
buildForecastCase,
populateFallbackNarratives,
buildForecastTraceArtifacts,
buildForecastTraceArtifactKeys,
buildForecastRunWorldState,
buildCrossSituationEffects,
buildSimulationMarketConsequences,
@@ -30,13 +31,22 @@ import {
computeDeepMarketCoherenceScore,
computeDeepPathAcceptanceScore,
selectDeepForecastCandidates,
serializeSituationMarketContextIndex,
buildDeepForecastSnapshotPayload,
validateImpactHypotheses,
evaluateDeepForecastPaths,
validateDeepForecastSnapshot,
} from '../scripts/seed-forecasts.mjs';
import {
resolveR2StorageConfig,
} from '../scripts/_r2-storage.mjs';
import {
evaluateForecastRunArtifacts,
} from '../scripts/evaluate-forecast-run.mjs';
import {
diffForecastRuns,
} from '../scripts/diff-forecast-runs.mjs';
describe('forecast trace storage config', () => {
it('resolves Cloudflare R2 trace env vars and derives the endpoint from account id', () => {
@@ -179,6 +189,11 @@ describe('forecast trace artifact builder', () => {
assert.equal(artifacts.summary.quality.publish.suppressedSituationCap, 1);
assert.equal(artifacts.summary.quality.publish.suppressedSituationDomainCap, 1);
assert.equal(artifacts.summary.quality.publish.cappedSituations, 1);
assert.match(artifacts.fastSummaryKey, /forecast-runs\/2026\/03\/15\/run-123\/fast-summary\.json/);
assert.match(artifacts.fastWorldStateKey, /forecast-runs\/2026\/03\/15\/run-123\/fast-world-state\.json/);
assert.match(artifacts.runStatusKey, /forecast-runs\/2026\/03\/15\/run-123\/run-status\.json/);
assert.equal(artifacts.runStatus.mode, 'fast');
assert.equal(artifacts.runStatus.status, 'completed');
assert.equal(artifacts.summary.quality.candidateRun.domainCounts.cyber, 1);
assert.deepEqual(artifacts.summary.quality.candidateRun.generationOriginCounts, {
legacy_detector: 3,
@@ -270,6 +285,16 @@ describe('forecast trace artifact builder', () => {
}
});
it('derives sidecar artifact keys for fast and deep lifecycle files', () => {
const keys = buildForecastTraceArtifactKeys('1774288939672-9bvvqa', Date.parse('2026-03-23T18:02:19.672Z'), 'seed-data/forecast-traces');
assert.match(keys.summaryKey, /seed-data\/forecast-traces\/2026\/03\/23\/1774288939672-9bvvqa\/summary\.json/);
assert.match(keys.fastSummaryKey, /fast-summary\.json$/);
assert.match(keys.deepSummaryKey, /deep-summary\.json$/);
assert.match(keys.runStatusKey, /run-status\.json$/);
assert.match(keys.impactExpansionDebugKey, /impact-expansion-debug\.json$/);
assert.match(keys.pathScorecardsKey, /path-scorecards\.json$/);
});
it('stores full canonical narrative fields alongside compact short fields in trace artifacts', () => {
const pred = makePrediction('market', 'Strait of Hormuz', 'Energy repricing risk: Strait of Hormuz', 0.71, 0.64, '30d', [
{ type: 'shipping_cost_shock', value: 'Strait of Hormuz rerouting is keeping freight costs elevated.', weight: 0.38 },
@@ -4425,3 +4450,181 @@ describe('military domain guarantee in publish selection', () => {
});
});
describe('forecast replay lifecycle helpers', () => {
it('serializes situation market context maps before writing deep snapshots', () => {
const marketSelectionIndex = {
bySituationId: new Map([
['state-1', {
situationId: 'state-1',
topBucketId: 'energy',
topChannel: 'shipping_cost_shock',
confirmationScore: 0.71,
}],
]),
summary: '1 state-aware market context was derived.',
};
const serialized = serializeSituationMarketContextIndex(marketSelectionIndex);
assert.deepEqual(serialized.bySituationId, {
'state-1': {
situationId: 'state-1',
topBucketId: 'energy',
topChannel: 'shipping_cost_shock',
confirmationScore: 0.71,
},
});
const snapshot = buildDeepForecastSnapshotPayload({
generatedAt: Date.parse('2026-03-23T18:25:20.121Z'),
marketSelectionIndex,
}, { runId: 'run-123' });
assert.deepEqual(snapshot.marketSelectionIndex.bySituationId, serialized.bySituationId);
assert.equal(snapshot.marketSelectionIndex.summary, marketSelectionIndex.summary);
});
it('flags invalid deep snapshots with unresolved selected state ids and duplicate labels', () => {
const validation = validateDeepForecastSnapshot({
fullRunStateUnits: [
{ id: 'state-1', label: 'Red Sea maritime disruption state' },
{ id: 'state-2', label: 'Red Sea maritime disruption state' },
],
deepForecast: {
selectedStateIds: ['state-1', 'missing-state'],
},
});
assert.equal(validation.pass, false);
assert.deepEqual(validation.unresolvedSelectedStateIds, ['missing-state']);
assert.deepEqual(validation.duplicateStateLabels, [
{ label: 'Red Sea maritime disruption state', count: 2 },
]);
});
it('evaluates a run artifact set against deep lifecycle checks', () => {
const evaluation = evaluateForecastRunArtifacts({
generatedAt: Date.parse('2026-03-23T18:25:20.121Z'),
summary: {
runId: '1774288939672-9bvvqa',
forecastDepth: 'deep',
deepForecast: {
status: 'completed_no_material_change',
eligibleStateCount: 2,
selectedStateIds: ['state-1'],
},
quality: {
candidateRun: {
domainCounts: {
supply_chain: 3,
},
},
traced: {
domainCounts: {
supply_chain: 0,
},
},
},
worldStateSummary: {
impactExpansionMappedSignalCount: 0,
simulationInteractionCount: 80,
reportableInteractionCount: 80,
},
},
worldState: {
impactExpansion: {
mappedSignalCount: 0,
},
simulationState: {
interactionLedger: Array.from({ length: 80 }, (_, i) => ({ id: i + 1 })),
reportableInteractionLedger: Array.from({ length: 80 }, (_, i) => ({ id: i + 1 })),
},
},
runStatus: {
forecastRunId: '1774288939672-9bvvqa',
selectedDeepStateIds: ['state-1'],
},
snapshot: {
fullRunStateUnits: [
{ id: 'state-1', label: 'Strait of Hormuz maritime disruption state' },
],
impactExpansionCandidates: [
{
candidateStateId: 'state-1',
stateKind: 'maritime_disruption',
routeFacilityKey: 'strait_of_hormuz',
commodityKey: 'crude_oil',
marketContext: {
topBucketId: 'energy',
},
},
],
},
});
assert.equal(evaluation.pass, false);
assert.equal(evaluation.status, 'fail');
assert.equal(evaluation.metrics.mappedSignalCount, 0);
assert.ok(evaluation.checks.some((check) => check.name === 'reportable_interactions_are_subset' && check.pass === false));
assert.ok(evaluation.checks.some((check) => check.name === 'eligible_high_value_deep_run_materializes_mapped_signals' && check.pass === false));
});
it('diffs two forecast runs by lifecycle and publication metrics', () => {
const diff = diffForecastRuns(
{
summary: {
runId: 'baseline',
forecastDepth: 'fast',
deepForecast: { status: 'queued' },
tracedForecastCount: 12,
topForecasts: [{ title: 'FX stress from Germany cyber pressure state' }],
worldStateSummary: {
impactExpansionCandidateCount: 3,
impactExpansionMappedSignalCount: 0,
simulationInteractionCount: 80,
reportableInteractionCount: 80,
},
quality: {
traced: {
domainCounts: { market: 10, supply_chain: 0 },
},
},
},
snapshot: {
fullRunStateUnits: [{ label: 'Germany cyber pressure state' }],
},
},
{
summary: {
runId: 'candidate',
forecastDepth: 'deep',
deepForecast: { status: 'completed' },
tracedForecastCount: 14,
topForecasts: [{ title: 'Supply chain stress from Strait of Hormuz disruption state' }],
worldStateSummary: {
impactExpansionCandidateCount: 3,
impactExpansionMappedSignalCount: 4,
simulationInteractionCount: 80,
reportableInteractionCount: 42,
},
quality: {
traced: {
domainCounts: { market: 10, supply_chain: 3 },
},
},
},
snapshot: {
fullRunStateUnits: [{ label: 'Strait of Hormuz maritime disruption state' }],
},
},
);
assert.equal(diff.forecastDepth.baseline, 'fast');
assert.equal(diff.forecastDepth.candidate, 'deep');
assert.equal(diff.impactExpansionDelta.mappedSignalCount, 4);
assert.equal(diff.interactionDelta.reportable, -38);
assert.equal(diff.publishedDomainDelta.supply_chain, 3);
assert.ok(diff.addedTopForecastTitles.includes('Supply chain stress from Strait of Hormuz disruption state'));
assert.ok(diff.removedTopForecastTitles.includes('FX stress from Germany cyber pressure state'));
});
});