@@ -20,6 +20,7 @@ const TRACE_LATEST_KEY = 'forecast:trace:latest:v1';
2020const TRACE_RUNS_KEY = 'forecast:trace:runs:v1' ;
2121const TRACE_RUNS_MAX = 50 ;
2222const TRACE_REDIS_TTL_SECONDS = 60 * 24 * 60 * 60 ;
23+ const WORLD_STATE_HISTORY_LIMIT = 6 ;
2324const PUBLISH_MIN_PROBABILITY = 0 ;
2425const PANEL_MIN_PROBABILITY = 0.1 ;
2526const ENRICHMENT_COMBINED_MAX = 3 ;
@@ -2322,7 +2323,6 @@ function computeSituationSimilarity(currentCluster, priorCluster) {
23222323 overlapCount ( currentCluster . forecastIds || [ ] , priorCluster . forecastIds || [ ] ) * 0.5
23232324 ) ;
23242325}
2325-
23262326function buildSituationClusters ( predictions ) {
23272327 const clusters = [ ] ;
23282328
@@ -2480,6 +2480,128 @@ function buildSituationSummary(situationClusters, situationContinuity) {
24802480 } ;
24812481}
24822482
2483+ function summarizeWorldStateHistory ( priorWorldStates = [ ] ) {
2484+ return priorWorldStates
2485+ . filter ( Boolean )
2486+ . slice ( 0 , WORLD_STATE_HISTORY_LIMIT )
2487+ . map ( ( state ) => ( {
2488+ generatedAt : state . generatedAt ,
2489+ generatedAtIso : state . generatedAtIso ,
2490+ summary : state . summary ,
2491+ domainCount : Array . isArray ( state . domainStates ) ? state . domainStates . length : 0 ,
2492+ regionCount : Array . isArray ( state . regionalStates ) ? state . regionalStates . length : 0 ,
2493+ situationCount : Array . isArray ( state . situationClusters ) ? state . situationClusters . length : 0 ,
2494+ actorCount : Array . isArray ( state . actorRegistry ) ? state . actorRegistry . length : 0 ,
2495+ branchCount : Array . isArray ( state . branchStates ) ? state . branchStates . length : 0 ,
2496+ } ) ) ;
2497+ }
2498+
2499+ function buildReportContinuity ( current , priorWorldStates = [ ] ) {
2500+ const history = summarizeWorldStateHistory ( priorWorldStates ) ;
2501+
2502+ const persistentPressures = [ ] ;
2503+ const emergingPressures = [ ] ;
2504+ const fadingPressures = [ ] ;
2505+ const repeatedStrengthening = [ ] ;
2506+ const matchedLatestPriorIds = new Set ( ) ;
2507+
2508+ for ( const cluster of current . situationClusters || [ ] ) {
2509+ const priorMatches = [ ] ;
2510+ for ( const state of priorWorldStates . filter ( Boolean ) ) {
2511+ const candidates = Array . isArray ( state . situationClusters ) ? state . situationClusters : [ ] ;
2512+ let match = candidates . find ( ( item ) => item . id === cluster . id ) || null ;
2513+ if ( ! match ) {
2514+ let bestMatch = null ;
2515+ let bestScore = 0 ;
2516+ for ( const candidate of candidates ) {
2517+ const score = computeSituationSimilarity ( cluster , candidate ) ;
2518+ if ( score > bestScore ) {
2519+ bestScore = score ;
2520+ bestMatch = candidate ;
2521+ }
2522+ }
2523+ if ( bestMatch && bestScore >= 4 ) match = bestMatch ;
2524+ }
2525+ if ( ! match ) continue ;
2526+ priorMatches . push ( {
2527+ id : match . id ,
2528+ label : match . label ,
2529+ generatedAt : state . generatedAt || 0 ,
2530+ avgProbability : Number ( match . avgProbability || 0 ) ,
2531+ forecastCount : Number ( match . forecastCount || 0 ) ,
2532+ } ) ;
2533+ if ( state === priorWorldStates [ 0 ] ) matchedLatestPriorIds . add ( match . id ) ;
2534+ }
2535+
2536+ if ( priorMatches . length === 0 ) {
2537+ emergingPressures . push ( {
2538+ id : cluster . id ,
2539+ label : cluster . label ,
2540+ forecastCount : cluster . forecastCount ,
2541+ avgProbability : cluster . avgProbability ,
2542+ } ) ;
2543+ continue ;
2544+ }
2545+
2546+ persistentPressures . push ( {
2547+ id : cluster . id ,
2548+ label : cluster . label ,
2549+ appearances : priorMatches . length + 1 ,
2550+ forecastCount : cluster . forecastCount ,
2551+ avgProbability : cluster . avgProbability ,
2552+ } ) ;
2553+
2554+ // priorMatches is ordered most-recent-first (mirrors priorWorldStates order from LRANGE)
2555+ const lastMatch = priorMatches [ 0 ] ;
2556+ const earliestMatch = priorMatches [ priorMatches . length - 1 ] ;
2557+ // "strengthening" means current is >= both the most-recent and oldest prior snapshots,
2558+ // catching recoveries (V-shapes) as well as monotonic increases intentionally
2559+ if (
2560+ cluster . avgProbability >= ( lastMatch ?. avgProbability || 0 ) &&
2561+ cluster . avgProbability >= ( earliestMatch ?. avgProbability || 0 ) &&
2562+ cluster . forecastCount >= ( lastMatch ?. forecastCount || 0 )
2563+ ) {
2564+ repeatedStrengthening . push ( {
2565+ id : cluster . id ,
2566+ label : cluster . label ,
2567+ avgProbability : cluster . avgProbability ,
2568+ priorAvgProbability : lastMatch ?. avgProbability || 0 ,
2569+ appearances : priorMatches . length + 1 ,
2570+ } ) ;
2571+ }
2572+ }
2573+
2574+ const latestPriorState = priorWorldStates [ 0 ] || null ;
2575+ for ( const cluster of latestPriorState ?. situationClusters || [ ] ) {
2576+ if ( matchedLatestPriorIds . has ( cluster . id ) ) continue ;
2577+ fadingPressures . push ( {
2578+ id : cluster . id ,
2579+ label : cluster . label ,
2580+ forecastCount : cluster . forecastCount || 0 ,
2581+ avgProbability : cluster . avgProbability || 0 ,
2582+ } ) ;
2583+ }
2584+
2585+ const summary = history . length
2586+ ? `Across the last ${ history . length + 1 } runs, ${ persistentPressures . length } situations persisted, ${ emergingPressures . length } emerged, and ${ fadingPressures . length } faded from the latest prior snapshot.`
2587+ : 'No prior world-state history is available yet for report continuity.' ;
2588+
2589+ return {
2590+ history,
2591+ summary,
2592+ persistentPressureCount : persistentPressures . length ,
2593+ emergingPressureCount : emergingPressures . length ,
2594+ fadingPressureCount : fadingPressures . length ,
2595+ repeatedStrengtheningCount : repeatedStrengthening . length ,
2596+ persistentPressurePreview : persistentPressures . slice ( 0 , 8 ) ,
2597+ emergingPressurePreview : emergingPressures . slice ( 0 , 8 ) ,
2598+ fadingPressurePreview : fadingPressures . slice ( 0 , 8 ) ,
2599+ repeatedStrengtheningPreview : repeatedStrengthening
2600+ . sort ( ( a , b ) => b . appearances - a . appearances || b . avgProbability - a . avgProbability || a . id . localeCompare ( b . id ) )
2601+ . slice ( 0 , 8 ) ,
2602+ } ;
2603+ }
2604+
24832605function buildWorldStateReport ( worldState ) {
24842606 const leadDomains = ( worldState . domainStates || [ ] )
24852607 . slice ( 0 , 3 )
@@ -2541,6 +2663,24 @@ function buildWorldStateReport(worldState) {
25412663 } ) ) ,
25422664 ] . slice ( 0 , 6 ) ;
25432665
2666+ const continuityWatchlist = [
2667+ ...( worldState . reportContinuity ?. repeatedStrengtheningPreview || [ ] ) . map ( ( situation ) => ( {
2668+ type : 'persistent_strengthening' ,
2669+ label : situation . label ,
2670+ summary : `${ situation . label } has strengthened across ${ situation . appearances } runs, from ${ roundPct ( situation . priorAvgProbability ) } to ${ roundPct ( situation . avgProbability ) } .` ,
2671+ } ) ) ,
2672+ ...( worldState . reportContinuity ?. emergingPressurePreview || [ ] ) . map ( ( situation ) => ( {
2673+ type : 'emerging_pressure' ,
2674+ label : situation . label ,
2675+ summary : `${ situation . label } is a newly emerging situation in the current run.` ,
2676+ } ) ) ,
2677+ ...( worldState . reportContinuity ?. fadingPressurePreview || [ ] ) . map ( ( situation ) => ( {
2678+ type : 'fading_pressure' ,
2679+ label : situation . label ,
2680+ summary : `${ situation . label } has faded versus the latest prior world-state snapshot.` ,
2681+ } ) ) ,
2682+ ] . slice ( 0 , 6 ) ;
2683+
25442684 const continuitySummary = `Actors: ${ worldState . actorContinuity ?. newlyActiveCount || 0 } new, ${ worldState . actorContinuity ?. strengthenedCount || 0 } strengthened. Branches: ${ worldState . branchContinuity ?. newBranchCount || 0 } new, ${ worldState . branchContinuity ?. strengthenedBranchCount || 0 } strengthened, ${ worldState . branchContinuity ?. resolvedBranchCount || 0 } resolved. Situations: ${ worldState . situationContinuity ?. newSituationCount || 0 } new, ${ worldState . situationContinuity ?. strengthenedSituationCount || 0 } strengthened, ${ worldState . situationContinuity ?. resolvedSituationCount || 0 } resolved.` ;
25452685
25462686 const summary = `${ worldState . summary } The leading domains in this run are ${ leadDomains . join ( ', ' ) || 'none' } , the main continuity changes are captured through ${ worldState . actorContinuity ?. newlyActiveCount || 0 } newly active actors and ${ worldState . branchContinuity ?. strengthenedBranchCount || 0 } strengthened branches, and the situation layer currently carries ${ worldState . situationClusters ?. length || 0 } active clusters.` ;
@@ -2557,6 +2697,7 @@ function buildWorldStateReport(worldState) {
25572697 actorWatchlist,
25582698 branchWatchlist,
25592699 situationWatchlist,
2700+ continuityWatchlist,
25602701 keyUncertainties : ( worldState . uncertainties || [ ] ) . slice ( 0 , 6 ) . map ( item => item . summary || item ) ,
25612702 } ;
25622703}
@@ -2724,6 +2865,9 @@ function buildForecastRunWorldState(data) {
27242865 const situationClusters = buildSituationClusters ( predictions ) ;
27252866 const situationContinuity = buildSituationContinuitySummary ( situationClusters , priorWorldState ) ;
27262867 const situationSummary = buildSituationSummary ( situationClusters , situationContinuity ) ;
2868+ const reportContinuity = buildReportContinuity ( {
2869+ situationClusters,
2870+ } , data ?. priorWorldStates || [ ] ) ;
27272871 const continuity = buildForecastRunContinuity ( predictions ) ;
27282872 const evidenceLedger = buildForecastEvidenceLedger ( predictions ) ;
27292873 const activeDomains = domainStates . filter ( ( item ) => item . forecastCount > 0 ) . map ( ( item ) => item . domain ) ;
@@ -2742,6 +2886,7 @@ function buildForecastRunWorldState(data) {
27422886 situationClusters,
27432887 situationContinuity,
27442888 situationSummary,
2889+ reportContinuity,
27452890 continuity,
27462891 evidenceLedger,
27472892 uncertainties : evidenceLedger . counter . slice ( 0 , 10 ) ,
@@ -2855,6 +3000,7 @@ function buildForecastTraceArtifacts(data, context = {}, config = {}) {
28553000 generatedAt,
28563001 predictions,
28573002 priorWorldState : data ?. priorWorldState || null ,
3003+ priorWorldStates : data ?. priorWorldStates || [ ] ,
28583004 } ) ;
28593005 const prefix = buildTraceRunPrefix (
28603006 context . runId || `run_${ generatedAt } ` ,
@@ -2894,6 +3040,7 @@ function buildForecastTraceArtifacts(data, context = {}, config = {}) {
28943040 worldStateSummary : {
28953041 summary : worldState . summary ,
28963042 reportSummary : worldState . report ?. summary || '' ,
3043+ reportContinuitySummary : worldState . reportContinuity ?. summary || '' ,
28973044 domainCount : worldState . domainStates . length ,
28983045 regionCount : worldState . regionalStates . length ,
28993046 situationCount : worldState . situationClusters . length ,
@@ -2902,6 +3049,11 @@ function buildForecastTraceArtifacts(data, context = {}, config = {}) {
29023049 strengthenedSituations : worldState . situationContinuity . strengthenedSituationCount ,
29033050 weakenedSituations : worldState . situationContinuity . weakenedSituationCount ,
29043051 resolvedSituations : worldState . situationContinuity . resolvedSituationCount ,
3052+ historyRuns : worldState . reportContinuity ?. history ?. length || 0 ,
3053+ persistentPressures : worldState . reportContinuity ?. persistentPressureCount || 0 ,
3054+ emergingPressures : worldState . reportContinuity ?. emergingPressureCount || 0 ,
3055+ fadingPressures : worldState . reportContinuity ?. fadingPressureCount || 0 ,
3056+ repeatedStrengthening : worldState . reportContinuity ?. repeatedStrengtheningCount || 0 ,
29053057 actorCount : worldState . actorRegistry . length ,
29063058 persistentActorCount : worldState . actorContinuity . persistentCount ,
29073059 newlyActiveActors : worldState . actorContinuity . newlyActiveCount ,
@@ -2968,17 +3120,52 @@ async function readPreviousForecastWorldState(storageConfig) {
29683120 }
29693121}
29703122
3123+ // Returns world states ordered most-recent-first (LPUSH prepends, LRANGE 0 N reads from head).
3124+ // Callers that rely on priorMatches[0] being the most recent must not reorder this array.
3125+ async function readForecastWorldStateHistory ( storageConfig , limit = WORLD_STATE_HISTORY_LIMIT ) {
3126+ try {
3127+ const { url, token } = getRedisCredentials ( ) ;
3128+ const resp = await redisCommand ( url , token , [ 'LRANGE' , TRACE_RUNS_KEY , 0 , Math . max ( 0 , limit - 1 ) ] ) ;
3129+ const rawPointers = Array . isArray ( resp ?. result ) ? resp . result : [ ] ;
3130+ const pointers = rawPointers
3131+ . map ( ( value ) => {
3132+ try { return JSON . parse ( value ) ; } catch { return null ; }
3133+ } )
3134+ . filter ( ( item ) => item ?. worldStateKey ) ;
3135+ const seen = new Set ( ) ;
3136+ const keys = [ ] ;
3137+ for ( const pointer of pointers ) {
3138+ if ( seen . has ( pointer . worldStateKey ) ) continue ;
3139+ seen . add ( pointer . worldStateKey ) ;
3140+ keys . push ( pointer . worldStateKey ) ;
3141+ if ( keys . length >= limit ) break ;
3142+ }
3143+ const states = await Promise . all ( keys . map ( ( key ) => getR2JsonObject ( storageConfig , key ) . catch ( ( ) => null ) ) ) ;
3144+ return states . filter ( Boolean ) ;
3145+ } catch ( err ) {
3146+ console . warn ( ` [Trace] World-state history read failed: ${ err . message } ` ) ;
3147+ return [ ] ;
3148+ }
3149+ }
3150+
29713151async function writeForecastTraceArtifacts ( data , context = { } ) {
29723152 const storageConfig = resolveR2StorageConfig ( ) ;
29733153 if ( ! storageConfig ) return null ;
29743154 const predictionCount = Array . isArray ( data ?. predictions ) ? data . predictions . length : 0 ;
29753155 const traceCap = getTraceCapLog ( predictionCount ) ;
29763156 console . log ( ` Trace cap: raw=${ traceCap . raw ?? 'default' } resolved=${ traceCap . resolved } total=${ traceCap . totalForecasts } ` ) ;
29773157
2978- const priorWorldState = await readPreviousForecastWorldState ( storageConfig ) ;
3158+ // Run both reads in parallel; derive priorWorldState from history head to avoid
3159+ // a redundant R2 GET (TRACE_RUNS_KEY[0] and TRACE_LATEST_KEY normally point to the same object).
3160+ const [ priorWorldStates , priorWorldStateFallback ] = await Promise . all ( [
3161+ readForecastWorldStateHistory ( storageConfig , WORLD_STATE_HISTORY_LIMIT ) ,
3162+ readPreviousForecastWorldState ( storageConfig ) ,
3163+ ] ) ;
3164+ const priorWorldState = priorWorldStates [ 0 ] ?? priorWorldStateFallback ;
29793165 const artifacts = buildForecastTraceArtifacts ( {
29803166 ...data ,
29813167 priorWorldState,
3168+ priorWorldStates,
29823169 } , context , {
29833170 basePrefix : storageConfig . basePrefix ,
29843171 maxForecasts : getTraceMaxForecasts ( predictionCount ) ,
0 commit comments