mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
98
scripts/diff-forecast-runs.mjs
Normal file
98
scripts/diff-forecast-runs.mjs
Normal 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,
|
||||
};
|
||||
134
scripts/evaluate-forecast-run.mjs
Normal file
134
scripts/evaluate-forecast-run.mjs
Normal 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,
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
223
scripts/replay-forecast-run.mjs
Normal file
223
scripts/replay-forecast-run.mjs
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user