| |
| |
| |
|
|
| const SCENARIO_JUMP_THRESHOLD = 0.15; |
| const LEVERAGE_SHIFT_THRESHOLD = 0.15; |
| const BUFFER_FAILURE_THRESHOLD = 0.20; |
|
|
| |
| |
| |
| |
| |
| export function diffRegionalSnapshot(prev, curr) { |
| const diff = { |
| regime_changed: null, |
| scenario_jumps: [], |
| trigger_activations: [], |
| trigger_deactivations: [], |
| corridor_breaks: [], |
| leverage_shifts: [], |
| buffer_failures: [], |
| reroute_waves: null, |
| mobility_disruptions: [], |
| }; |
|
|
| if (!prev) { |
| |
| if (curr.regime?.label && curr.regime.label !== 'calm') { |
| diff.regime_changed = { from: '', to: curr.regime.label }; |
| } |
| diff.trigger_activations = curr.triggers.active.map((t) => ({ id: t.id, description: t.description })); |
| return diff; |
| } |
|
|
| |
| if (prev.regime?.label !== curr.regime?.label) { |
| diff.regime_changed = { from: prev.regime?.label ?? '', to: curr.regime.label }; |
| } |
|
|
| |
| for (const currSet of curr.scenario_sets) { |
| const prevSet = prev.scenario_sets?.find((s) => s.horizon === currSet.horizon); |
| if (!prevSet) continue; |
| for (const currLane of currSet.lanes) { |
| const prevLane = prevSet.lanes.find((l) => l.name === currLane.name); |
| if (!prevLane) continue; |
| const delta = Math.abs(currLane.probability - prevLane.probability); |
| if (delta > SCENARIO_JUMP_THRESHOLD) { |
| diff.scenario_jumps.push({ |
| horizon: currSet.horizon, |
| lane: currLane.name, |
| from: prevLane.probability, |
| to: currLane.probability, |
| }); |
| } |
| } |
| } |
|
|
| |
| const prevActive = new Set(prev.triggers.active.map((t) => t.id)); |
| const currActive = new Set(curr.triggers.active.map((t) => t.id)); |
| for (const t of curr.triggers.active) { |
| if (!prevActive.has(t.id)) diff.trigger_activations.push({ id: t.id, description: t.description }); |
| } |
| for (const t of prev.triggers.active) { |
| if (!currActive.has(t.id)) diff.trigger_deactivations.push({ id: t.id }); |
| } |
|
|
| |
| |
| |
| const prevMaritime = prev.balance?.maritime_access ?? 1; |
| const currMaritime = curr.balance?.maritime_access ?? 1; |
| if (prevMaritime - currMaritime > 0.3) { |
| diff.corridor_breaks.push({ |
| corridor_id: 'aggregate', |
| from: prevMaritime.toFixed(2), |
| to: currMaritime.toFixed(2), |
| }); |
| } |
|
|
| |
| const prevActors = new Map((prev.actors ?? []).map((a) => [a.actor_id, a.leverage_score])); |
| for (const a of curr.actors ?? []) { |
| const prevScore = prevActors.get(a.actor_id) ?? 0; |
| const delta = a.leverage_score - prevScore; |
| if (Math.abs(delta) > LEVERAGE_SHIFT_THRESHOLD) { |
| diff.leverage_shifts.push({ |
| actor_id: a.actor_id, |
| from: prevScore, |
| to: a.leverage_score, |
| delta, |
| }); |
| } |
| } |
|
|
| |
| const bufferAxes = ['alliance_cohesion', 'maritime_access', 'energy_leverage']; |
| for (const axis of bufferAxes) { |
| const prevVal = prev.balance?.[axis] ?? 1; |
| const currVal = curr.balance?.[axis] ?? 1; |
| if (prevVal - currVal > BUFFER_FAILURE_THRESHOLD) { |
| diff.buffer_failures.push({ axis, from: prevVal, to: currVal }); |
| } |
| } |
|
|
| |
| |
|
|
| return diff; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function inferTriggerReason(diff) { |
| if (diff.regime_changed) return 'regime_shift'; |
| if (diff.trigger_activations.length > 0) return 'trigger_activation'; |
| if (diff.corridor_breaks.length > 0) return 'corridor_break'; |
| if (diff.leverage_shifts.length > 0) return 'leverage_shift'; |
| return 'scheduled_6h'; |
| } |
|
|