fix(forecast): block generic-actor cross-theater interactions + raise enrichment budget (#1916)

* fix(forecast): block generic-actor cross-theater interactions + raise enrichment budget

Root cause: actor registry uses name:category as key (e.g. "Incumbent leadership:state"),
causing unrelated situations (Israel conflict, Taiwan political) to share the same actor
ID and fire sharedActor=true in pushInteraction. This propagated into the reportable
ledger and surfaced as junk effects like Israel→Taiwan at 80% confidence.

Two-pronged fix:

1. Specificity gate in pushInteraction: sharedActor now requires avgSpecificity >= 0.75.
   Generic blueprint actors ("Incumbent leadership" ~0.68, "Civil protection authorities"
   ~0.73) no longer qualify as structural cross-situation links. Named domain-specific
   actors ("Threat actors:adversarial" ~0.95) continue to qualify.

2. MACRO_REGION_MAP + isCrossTheaterPair + gate in buildCrossSituationEffects: for
   cross-theater pairs (different macro-regions) with non-exempt channels, requires
   sharedActor=true AND avgActorSpecificity >= 0.90. Exempt channels: cyber_disruption,
   market_repricing (legitimately global). Same-macro-region pairs (Brazil/Mexico both
   AMERICAS) are unaffected.

Verified against live run 1773983083084-bu6b1f:
  BLOCKED: Israel→Taiwan (MENA/EAST_ASIA, spec 0.68)
  BLOCKED: Israel→US political (MENA/AMERICAS, spec 0.68)
  BLOCKED: Cuba→Iran (AMERICAS/MENA, spec 0.73)
  BLOCKED: Brazil→Israel (AMERICAS/MENA, spec 0.85 < 0.90)
  ALLOWED: China→US cyber_disruption (exempt channel)
  ALLOWED: Brazil→Mexico (same AMERICAS)

Also raises ENRICHMENT_COMBINED_MAX from 3 to 5 (total budget 6→8),
targeting enrichedRate improvement from ~38% to ~60%.

* fix(plans): fix markdown lint errors in forecast semantic quality plan

* fix(plans): fix remaining markdown lint error in plan file
This commit is contained in:
Elie Habib
2026-03-20 13:26:58 +04:00
committed by GitHub
parent 32ca22d69f
commit 01366fcc00
3 changed files with 529 additions and 11 deletions

View File

@@ -0,0 +1,326 @@
# Plan: Fix Forecast Semantic Quality — 3 PRs
## Context
Live run `1773983083084-bu6b1f` (2026-03-20T05:04:52) confirms two categories of
junk effects still reaching the report surface. Root cause identified from live R2
data and code trace. This plan fixes them precisely and independently.
---
## Root Cause: Confirmed from Live R2 Data
### Bug 1: Generic blueprint actors create false `sharedActor` cross-situation links
The actor registry at line 2044 uses `actor.key || \`${actor.name}:${actor.category}\``
as the actor ID. When two different situations both instantiate a generic blueprint
actor (e.g. `"Incumbent leadership"` for conflict domain AND political domain), they
get the SAME actor ID: `"Incumbent leadership:state"`.
The actor registry deduplicates them into ONE entry with `forecastIds` from both
situations. Then `buildSituationSimulationState` filters actors per situation by
forecastId overlap — both situations get this shared actor entry. When
`pushInteraction` compares `source.actorId === target.actorId`, it fires `true`.
**Live evidence:**
```
Israel conflict → Taiwan political | ch: regional_spillover | sharedActor: true
srcActorId: "Incumbent leadership:state" | actorSpec: 0.68
Cuba infrastructure → Iran infrastructure | ch: service_disruption | sharedActor: true
srcActorId: "Civil protection authorities:state" | actorSpec: 0.73
```
`scoreActorSpecificity("Incumbent leadership")` = 0.68. It IS already penalized by
`GENERIC_ACTOR_NAME_MARKERS` containing `'leadership'`. The engine just never gates
`sharedActor` on specificity before using it as a structural credit.
### Bug 2: No geographic theater awareness in cross-situation effect emission
Once an interaction makes it into `reportableInteractionLedger` via `sharedActor: true`,
`buildCrossSituationEffects` has no concept of geographic distance between situations.
Israel (MENA) → Taiwan (East Asia) passes all current gates if the interaction group
has high enough score. There is no check that these are on opposite sides of the planet.
`CROSS_THEATER_EXEMPT_CHANNELS` doesn't exist yet. Cyber and market effects can
legitimately cross theaters (same APT group, same commodity price). Conflict,
political, infrastructure, and supply_chain effects should not unless the actor is
specifically and credibly named.
---
## PR 1 — `fix(forecast): block generic-actor cross-theater interactions`
**File:** `scripts/seed-forecasts.mjs`
**Scope:** 3 targeted changes + test updates
**Risk:** Low — tightens existing gates, does not remove any data path
### Change A: Specificity gate in `pushInteraction` (lines 34803491)
Move `sourceSpecificity / targetSpecificity / avgSpecificity` computation ABOVE the
`sharedActor` declaration. Add specificity threshold to `sharedActor`:
**Before (line 3480):**
```js
const sharedActor = source.actorId && target.actorId && source.actorId === target.actorId;
const sharedChannels = ...
...
const sourceSpecificity = scoreActorSpecificity(source);
const targetSpecificity = scoreActorSpecificity(target);
const avgSpecificity = (sourceSpecificity + targetSpecificity) / 2;
```
**After:**
```js
const sourceSpecificity = scoreActorSpecificity(source);
const targetSpecificity = scoreActorSpecificity(target);
const avgSpecificity = (sourceSpecificity + targetSpecificity) / 2;
const sharedActor = source.actorId && target.actorId && source.actorId === target.actorId
&& avgSpecificity >= 0.72;
const sharedChannels = ...
```
**Threshold justification from live data:**
- `"Incumbent leadership:state"` → 0.68 → BLOCKED ✓
- `"Civil protection authorities:state"` → 0.73 → BLOCKED ✓ (just above 0.72, need check)
- `"Threat actors:adversarial"` → 0.95 → ALLOWED (China/US cyber) ✓
- `"External power broker:external"` → 0.85 → ALLOWED at this stage, handled by Change B
NOTE: "Civil protection authorities:state" scores 0.73 because:
`base(0.55) + nonShared(+0.10) + nonGenericCategory(+0.15) + 3-word name(+0.05) - genericMarker('authorities', -0.12) = 0.73`
The threshold of 0.72 blocks it by 0.01. This is intentional but fragile. If concerned, raise to
0.75 to give a clear margin. Test with both values.
### Change B: MACRO_REGION_MAP + cross-theater gate in `buildCrossSituationEffects`
Add a new constant block immediately before `buildCrossSituationEffects`:
```js
const MACRO_REGION_MAP = {
// MENA
'Israel': 'MENA', 'Iran': 'MENA', 'Syria': 'MENA', 'Iraq': 'MENA',
'Lebanon': 'MENA', 'Gaza': 'MENA', 'Egypt': 'MENA', 'Saudi Arabia': 'MENA',
'Yemen': 'MENA', 'Jordan': 'MENA', 'Turkey': 'MENA', 'Libya': 'MENA',
'Middle East': 'MENA', 'Persian Gulf': 'MENA', 'Red Sea': 'MENA',
'Strait of Hormuz': 'MENA', 'Eastern Mediterranean': 'MENA',
// EAST_ASIA
'Taiwan': 'EAST_ASIA', 'China': 'EAST_ASIA', 'Japan': 'EAST_ASIA',
'South Korea': 'EAST_ASIA', 'North Korea': 'EAST_ASIA',
'Western Pacific': 'EAST_ASIA', 'South China Sea': 'EAST_ASIA',
// AMERICAS
'United States': 'AMERICAS', 'Brazil': 'AMERICAS', 'Mexico': 'AMERICAS',
'Cuba': 'AMERICAS', 'Canada': 'AMERICAS', 'Colombia': 'AMERICAS',
'Venezuela': 'AMERICAS', 'Argentina': 'AMERICAS', 'Peru': 'AMERICAS',
// EUROPE
'Russia': 'EUROPE', 'Ukraine': 'EUROPE', 'Germany': 'EUROPE',
'France': 'EUROPE', 'United Kingdom': 'EUROPE', 'Poland': 'EUROPE',
'Baltic Sea': 'EUROPE', 'Black Sea': 'EUROPE', 'Kerch Strait': 'EUROPE',
'Sweden': 'EUROPE', 'Finland': 'EUROPE', 'Norway': 'EUROPE',
// SOUTH_ASIA
'India': 'SOUTH_ASIA', 'Pakistan': 'SOUTH_ASIA', 'Afghanistan': 'SOUTH_ASIA',
'Bangladesh': 'SOUTH_ASIA', 'Myanmar': 'SOUTH_ASIA',
// AFRICA
'Congo': 'AFRICA', 'Sudan': 'AFRICA', 'Ethiopia': 'AFRICA',
'Nigeria': 'AFRICA', 'Somalia': 'AFRICA', 'Mali': 'AFRICA',
'Mozambique': 'AFRICA', 'Sahel': 'AFRICA',
};
// Channels where cross-theater effects are legitimate regardless of geography
const CROSS_THEATER_EXEMPT_CHANNELS = new Set(['cyber_disruption', 'market_repricing']);
// Minimum actor specificity to justify a named-actor cross-theater link
const CROSS_THEATER_ACTOR_SPECIFICITY_MIN = 0.90;
function getMacroRegion(regions = []) {
for (const region of regions) {
if (MACRO_REGION_MAP[region]) return MACRO_REGION_MAP[region];
}
return null;
}
function isCrossTheaterPair(sourceRegions, targetRegions) {
const src = getMacroRegion(sourceRegions);
const tgt = getMacroRegion(targetRegions);
return !!(src && tgt && src !== tgt);
}
```
Then inside `buildCrossSituationEffects`, immediately after the `hasDirectStructuralLink`
line (currently line 3880), add:
```js
const isCrossTheater = isCrossTheaterPair(source.regions || [], target.regions || []);
if (
isCrossTheater
&& !CROSS_THEATER_EXEMPT_CHANNELS.has(group.strongestChannel)
&& (!hasSharedActor || Number(group.avgActorSpecificity || 0) < CROSS_THEATER_ACTOR_SPECIFICITY_MIN)
) continue;
```
**Gate logic verified against live pairs:**
| Pair | Channel | CrossTheater | Exempt? | SharedActor | ActorSpec | Result |
|------|---------|-------------|---------|------------|----------|--------|
| China cyber ↔ US cyber | cyber_disruption | YES (EAST_ASIA/AMERICAS) | YES | — | — | ALLOWED ✓ |
| Brazil ↔ Mexico conflict | security_escalation | NO (both AMERICAS) | — | true | 0.85 | ALLOWED ✓ |
| Brazil ↔ Israel conflict | security_escalation | YES (AMERICAS/MENA) | NO | true (after PR1A) | 0.85 < 0.90 | BLOCKED ✓ |
| Cuba ↔ Iran infra | service_disruption | YES (AMERICAS/MENA) | NO | false (after PR1A) | 0.73 | BLOCKED ✓ |
| Israel ↔ Taiwan political | regional_spillover | YES (MENA/EAST_ASIA) | NO | false (after PR1A) | 0.68 | BLOCKED ✓ |
| Israel ↔ US political | regional_spillover | YES (MENA/AMERICAS) | NO | false (after PR1A) | 0.68 | BLOCKED ✓ |
| Taiwan ↔ US political | political_pressure | YES (EAST_ASIA/AMERICAS) | NO | true | 0.85 < 0.90 | BLOCKED (correct: generic actor) |
| Baltic ↔ Black Sea supply | logistics_disruption | NO (both EUROPE) | — | — | — | ALLOWED ✓ |
### Change C: Export `isCrossTheaterPair` and `getMacroRegion` at bottom of file
Add to the `export {}` block so tests can import them:
```js
export {
...existing exports...
isCrossTheaterPair,
getMacroRegion,
};
```
### Test changes (`tests/forecast-trace-export.test.mjs`)
Add 3 new tests in the `forecast run world state` describe block:
1. **`blocks Israel → Taiwan via generic Incumbent Leadership actor`**
- Setup: two simulations (conflict/MENA, political/EAST_ASIA) with sharedActor=true at spec 0.68
- Assert: `effects.length === 0`
2. **`allows China → US via Threat Actors cross-theater cyber_disruption`**
- Setup: two simulations (cyber/EAST_ASIA, cyber/AMERICAS) with cyber_disruption, sharedActor=true at spec 0.95
- Assert: `effects.length === 1`, `effects[0].channel === 'cyber_disruption'`
3. **`blocks Brazil → Israel via External Power Broker security_escalation`**
- Setup: two simulations (conflict/AMERICAS, conflict/MENA) with sharedActor=true at spec 0.85
- Assert: `effects.length === 0` (cross-theater, channel not exempt, spec < 0.90)
Also add unit tests for `isCrossTheaterPair` directly:
- `isCrossTheaterPair(['Israel'], ['Taiwan'])` → `true`
- `isCrossTheaterPair(['Brazil'], ['Mexico'])` → `false`
- `isCrossTheaterPair(['China'], ['unknown-region'])` → `false` (null getMacroRegion)
### Verification
Run:
```
node --check scripts/seed-forecasts.mjs
tsx --test tests/forecast-trace-export.test.mjs
```
Expected: 0 junk cross-theater effects in next live run. Check `report.crossSituationEffects`
should contain only China/US cyber and Brazil/Mexico conflict.
---
## PR 2 — `fix(forecast): raise LLM enrichment budget`
**File:** `scripts/seed-forecasts.mjs` lines 2728 only
**Risk:** Very low — constant change, no logic change
### Change
```js
// Before
const ENRICHMENT_COMBINED_MAX = 3;
const ENRICHMENT_SCENARIO_MAX = 3;
// After
const ENRICHMENT_COMBINED_MAX = 5;
const ENRICHMENT_SCENARIO_MAX = 3;
```
Total enrichment budget: 3+3=6 → 5+3=8 per run.
### Why not higher?
- Current LLM success rate: 3/3 (100%) — headroom exists
- 8 enriched / 13 published = 61.5% target enrichedRate (up from 38.5%)
- Raising to 8+3=11 risks rate limits; 5+3=8 is safe headroom
- The scenario-only path is cheaper (no perspectives/cases call), keep at 3
### Test
No logic change. The existing enrichment selection tests remain valid.
Run `tsx --test tests/forecast-trace-export.test.mjs` as smoke test.
### Verification
After next live run, check `traceQuality.enrichedRate` in summary.json.
Target: > 0.55. Acceptable: > 0.45.
---
## PR 3 — `fix(forecast): diagnose and fix military detector surfacing`
**Status: requires live data diagnosis before coding**
### Diagnosis step (run before writing code)
Pull the latest world-state and check:
```js
// In the live run, check what military data was available
ws.domainStates.find(d => d.domain === 'military')
ws.report.domainOverview // check if military appears in activeDomainCount
```
Also check the seed inputs to confirm theater posture data:
```
curl https://api.worldmonitor.app/api/forecast-bootstrap | jq '.militaryData'
```
### Two possible fixes depending on diagnosis:
**Path A — Detector emits 0 (theater posture all 'normal')**
The gate at line 719:
```js
if (posture !== 'elevated' && posture !== 'critical' && !surgeIsUsable) continue;
```
This skips all theaters at 'normal' posture unless there's a usable surge. If no current
theater is elevated and surges are below threshold, zero military forecasts are generated.
Fix: Add a baseline military forecast for the most active theater even at 'normal' posture
if surge multiple is above a lower threshold (e.g., >= 1.8x baseline). This keeps military
present even during calm periods.
**Path B — Detector emits candidates but publish selection suppresses them**
Check `publishTelemetry.suppressedFamilySelection` and `suppressedSituationDomainCap`.
If military forecasts exist but are suppressed, examine whether `canSelect` in the
publish selection is blocking them. Military is already in the exemption list for
domain cap at line 5315, but check whether `MAX_PRESELECTED_FORECASTS_PER_FAMILY`
or `MAX_PUBLISHED_FORECASTS_PER_FAMILY_DOMAIN` is capping them before the diversity
pass reaches them.
Fix for Path B: Ensure at least one military forecast is explicitly seeded in the first
pass of the selection loop before the diversity filter runs.
### Not starting PR 3 until diagnosis confirms Path A or B.
---
## Execution Order
1. **PR 1** — `fix/forecast-cross-theater-gate` branch — highest leverage, fixes the
junk effects that are the primary credibility problem
2. **PR 2** — `fix/forecast-enrichment-budget` branch — 2-line change, can be done
in the same session, independent of PR 1
3. **PR 3** — diagnose first from live R2 data, then write a targeted fix
PRs 1 and 2 are independent and can be merged in any order. PR 3 is independent of both.
---
## What This Does Not Fix
- Enrichment quality — the LLM output quality depends on input prompt quality, not these PRs
- Situation clustering precision — some situations have overly broad region sets (e.g. Black Sea
market at `["Black Sea","Middle East","Red Sea","Western Pacific"]`) which will continue to
create imprecise cross-situation scoring; this is a separate, larger issue
- Retention/cleanup of old R2 trace files — 100+ runs/day accumulating with no TTL

View File

@@ -24,7 +24,7 @@ const WORLD_STATE_HISTORY_LIMIT = 6;
const FORECAST_REFRESH_REQUEST_KEY = 'forecast:refresh-request:v1';
const PUBLISH_MIN_PROBABILITY = 0;
const PANEL_MIN_PROBABILITY = 0.1;
const ENRICHMENT_COMBINED_MAX = 3;
const ENRICHMENT_COMBINED_MAX = 5;
const ENRICHMENT_SCENARIO_MAX = 3;
const ENRICHMENT_MAX_PER_DOMAIN = 2;
const ENRICHMENT_MIN_READINESS = 0.34;
@@ -3477,7 +3477,11 @@ function buildSimulationInteractionLedger(actionLedger = [], situationSimulation
function pushInteraction(source, target, stage) {
if (source.situationId === target.situationId) return;
const sharedActor = source.actorId && target.actorId && source.actorId === target.actorId;
const sourceSpecificity = scoreActorSpecificity(source);
const targetSpecificity = scoreActorSpecificity(target);
const avgSpecificity = (sourceSpecificity + targetSpecificity) / 2;
const sharedActor = source.actorId && target.actorId && source.actorId === target.actorId
&& avgSpecificity >= 0.75;
const sharedChannels = uniqueSortedStrings((source.channels || []).filter((channel) => (target.channels || []).includes(channel)));
const familyLink = source.familyId && target.familyId && source.familyId === target.familyId;
const regionLink = intersectCount(source.regions || [], target.regions || []) > 0;
@@ -3486,9 +3490,6 @@ function buildSimulationInteractionLedger(actionLedger = [], situationSimulation
(source.intent === 'pressure' && target.intent === 'stabilizing')
|| (source.intent === 'stabilizing' && target.intent === 'pressure')
);
const sourceSpecificity = scoreActorSpecificity(source);
const targetSpecificity = scoreActorSpecificity(target);
const avgSpecificity = (sourceSpecificity + targetSpecificity) / 2;
const score = (sharedActor ? 4 : 0)
+ (sharedChannels.length * 2)
@@ -3829,6 +3830,42 @@ function inferSystemEffectRelation(sourceDomain, targetDomain) {
return relationMap[key] || '';
}
const MACRO_REGION_MAP = {
'Israel': 'MENA', 'Iran': 'MENA', 'Syria': 'MENA', 'Iraq': 'MENA', 'Lebanon': 'MENA',
'Gaza': 'MENA', 'Egypt': 'MENA', 'Saudi Arabia': 'MENA', 'Yemen': 'MENA', 'Jordan': 'MENA',
'Turkey': 'MENA', 'Libya': 'MENA', 'Middle East': 'MENA', 'Persian Gulf': 'MENA',
'Red Sea': 'MENA', 'Strait of Hormuz': 'MENA', 'Eastern Mediterranean': 'MENA',
'Taiwan': 'EAST_ASIA', 'China': 'EAST_ASIA', 'Japan': 'EAST_ASIA', 'South Korea': 'EAST_ASIA',
'North Korea': 'EAST_ASIA', 'Western Pacific': 'EAST_ASIA', 'South China Sea': 'EAST_ASIA',
'United States': 'AMERICAS', 'Brazil': 'AMERICAS', 'Mexico': 'AMERICAS', 'Cuba': 'AMERICAS',
'Canada': 'AMERICAS', 'Colombia': 'AMERICAS', 'Venezuela': 'AMERICAS', 'Argentina': 'AMERICAS',
'Peru': 'AMERICAS', 'Chile': 'AMERICAS',
'Russia': 'EUROPE', 'Ukraine': 'EUROPE', 'Germany': 'EUROPE', 'France': 'EUROPE',
'United Kingdom': 'EUROPE', 'Poland': 'EUROPE', 'Baltic Sea': 'EUROPE', 'Black Sea': 'EUROPE',
'Kerch Strait': 'EUROPE', 'Sweden': 'EUROPE', 'Finland': 'EUROPE', 'Norway': 'EUROPE',
'Romania': 'EUROPE', 'Bulgaria': 'EUROPE',
'India': 'SOUTH_ASIA', 'Pakistan': 'SOUTH_ASIA', 'Afghanistan': 'SOUTH_ASIA',
'Bangladesh': 'SOUTH_ASIA', 'Myanmar': 'SOUTH_ASIA',
'Congo': 'AFRICA', 'Sudan': 'AFRICA', 'Ethiopia': 'AFRICA', 'Nigeria': 'AFRICA',
'Somalia': 'AFRICA', 'Mali': 'AFRICA', 'Mozambique': 'AFRICA', 'Sahel': 'AFRICA',
};
const CROSS_THEATER_EXEMPT_CHANNELS = new Set(['cyber_disruption', 'market_repricing']);
const CROSS_THEATER_ACTOR_SPECIFICITY_MIN = 0.90;
function getMacroRegion(regions = []) {
for (const region of regions) {
if (MACRO_REGION_MAP[region]) return MACRO_REGION_MAP[region];
}
return null;
}
function isCrossTheaterPair(sourceRegions, targetRegions) {
const src = getMacroRegion(sourceRegions);
const tgt = getMacroRegion(targetRegions);
return !!(src && tgt && src !== tgt);
}
function canEmitCrossSituationEffect(source, strongestChannel, strongestChannelWeight, hasDirectStructuralLink = false) {
if (!strongestChannel) return false;
const profile = getSimulationDomainProfile(source?.dominantDomain || '');
@@ -3880,6 +3917,11 @@ function buildCrossSituationEffects(simulationState) {
const hasDirectStructuralLink = hasRegionLink || hasSharedActor;
if (!canEmitCrossSituationEffect(source, group.strongestChannel, strongestChannelWeight, hasDirectStructuralLink)) continue;
if (strongestChannelWeight < 2 && !hasDirectStructuralLink) continue;
if (
isCrossTheaterPair(source.regions || [], target.regions || [])
&& !CROSS_THEATER_EXEMPT_CHANNELS.has(group.strongestChannel)
&& (!hasSharedActor || Number(group.avgActorSpecificity || 0) < CROSS_THEATER_ACTOR_SPECIFICITY_MIN)
) continue;
if (
group.strongestChannel === 'political_pressure'
&& !hasRegionLink
@@ -6623,6 +6665,8 @@ export {
buildCrossSituationEffects,
buildReportableInteractionLedger,
buildInteractionWatchlist,
isCrossTheaterPair,
getMacroRegion,
attachSituationContext,
projectSituationClusters,
refreshPublishedNarratives,

View File

@@ -10,6 +10,8 @@ import {
buildCrossSituationEffects,
buildReportableInteractionLedger,
buildInteractionWatchlist,
isCrossTheaterPair,
getMacroRegion,
attachSituationContext,
projectSituationClusters,
refreshPublishedNarratives,
@@ -1660,7 +1662,9 @@ describe('forecast run world state', () => {
assert.equal(reportable.length, 1);
});
it('allows strong two-round shared-actor political effects without regional overlap', () => {
it('blocks cross-theater political effects even with shared-actor when actorSpec below 0.90', () => {
// US (AMERICAS) → Japan (EAST_ASIA) via political_pressure with actorSpec 0.87 is cross-theater.
// The gate requires actorSpec >= 0.90 for non-exempt channels across theater boundaries.
const effects = buildCrossSituationEffects({
situationSimulations: [
{
@@ -1730,11 +1734,7 @@ describe('forecast run world state', () => {
],
});
assert.ok(effects.some((item) => (
item.sourceSituationId === 'sit-cyber'
&& item.targetSituationId === 'sit-market'
&& item.channel === 'political_pressure'
)));
assert.equal(effects.length, 0, 'US → Japan cross-theater political_pressure at actorSpec 0.87 should be blocked');
});
it('allows logistics effects with strong confidence while filtering weaker political ones', () => {
@@ -1909,3 +1909,151 @@ describe('forecast run world state', () => {
assert.ok((worldState.simulationState.situationSimulations || []).every((item) => item.postureScore < 0.99));
});
});
describe('cross-theater gate', () => {
it('identifies cross-theater pairs correctly', () => {
assert.equal(isCrossTheaterPair(['Israel'], ['Taiwan']), true);
assert.equal(isCrossTheaterPair(['Israel'], ['Iran']), false);
assert.equal(isCrossTheaterPair(['Brazil'], ['Mexico']), false);
assert.equal(isCrossTheaterPair(['Cuba'], ['Iran']), true);
assert.equal(isCrossTheaterPair(['China'], ['United States']), true);
assert.equal(isCrossTheaterPair(['Baltic Sea'], ['Black Sea']), false);
assert.equal(isCrossTheaterPair(['Israel'], ['unknown-region']), false);
assert.equal(isCrossTheaterPair(['unknown-a'], ['unknown-b']), false);
});
it('maps regions to macro-regions', () => {
assert.equal(getMacroRegion(['Israel', 'Gaza']), 'MENA');
assert.equal(getMacroRegion(['Taiwan', 'Western Pacific']), 'EAST_ASIA');
assert.equal(getMacroRegion(['Brazil']), 'AMERICAS');
assert.equal(getMacroRegion(['Baltic Sea', 'Black Sea']), 'EUROPE');
assert.equal(getMacroRegion(['unknown-region']), null);
assert.equal(getMacroRegion([]), null);
});
function makeSimulation(situationId, label, domain, regions, posture, postureScore, effectChannels = []) {
return {
situationId,
label,
dominantDomain: domain,
familyId: `fam-${situationId}`,
familyLabel: `${label} family`,
regions,
actorIds: [`actor-${situationId}`],
effectChannels,
posture,
postureScore,
totalPressure: posture === 'escalatory' ? 0.88 : 0.55,
totalStabilization: posture === 'escalatory' ? 0.22 : 0.38,
};
}
function makeInteraction(srcId, srcLabel, tgtId, tgtLabel, channel, stage, score, conf, spec, sharedActor, regionLink) {
return {
sourceSituationId: srcId,
targetSituationId: tgtId,
sourceLabel: srcLabel,
targetLabel: tgtLabel,
strongestChannel: channel,
interactionType: sharedActor ? 'actor_carryover' : 'spillover',
stage,
score,
confidence: conf,
actorSpecificity: spec,
directLinkCount: (sharedActor ? 1 : 0) + (regionLink ? 1 : 0) + 1,
sharedActor,
regionLink,
sourceActorName: 'Test actor',
targetActorName: 'Test actor',
};
}
it('blocks Israel → Taiwan via generic Incumbent Leadership (regional_spillover, spec 0.68)', () => {
const effects = buildCrossSituationEffects({
situationSimulations: [
makeSimulation('sit-israel', 'Israel conflict situation', 'conflict', ['Israel'], 'escalatory', 0.88,
[{ type: 'regional_spillover', count: 3 }]),
makeSimulation('sit-taiwan', 'Taiwan political situation', 'political', ['Taiwan'], 'contested', 0.54),
],
reportableInteractionLedger: [
makeInteraction('sit-israel', 'Israel conflict situation', 'sit-taiwan', 'Taiwan political situation',
'regional_spillover', 'round_1', 5.2, 0.77, 0.68, true, false),
makeInteraction('sit-israel', 'Israel conflict situation', 'sit-taiwan', 'Taiwan political situation',
'regional_spillover', 'round_2', 5.1, 0.77, 0.68, true, false),
],
});
assert.equal(effects.length, 0, 'Israel → Taiwan via generic actor should be blocked by cross-theater gate');
});
it('allows China → US via Threat Actors (cyber_disruption, exempt channel)', () => {
const effects = buildCrossSituationEffects({
situationSimulations: [
makeSimulation('sit-china', 'China cyber situation', 'cyber', ['China'], 'escalatory', 0.88,
[{ type: 'cyber_disruption', count: 3 }]),
// target must be infrastructure for cyber_disruption:infrastructure relation to exist
makeSimulation('sit-us', 'United States infrastructure situation', 'infrastructure', ['United States'], 'contested', 0.62),
],
reportableInteractionLedger: [
makeInteraction('sit-china', 'China cyber situation', 'sit-us', 'United States infrastructure situation',
'cyber_disruption', 'round_1', 6.5, 0.91, 0.95, true, false),
makeInteraction('sit-china', 'China cyber situation', 'sit-us', 'United States infrastructure situation',
'cyber_disruption', 'round_2', 6.3, 0.90, 0.95, true, false),
],
});
assert.equal(effects.length, 1, 'China (EAST_ASIA) → US (AMERICAS) via cyber_disruption should pass (exempt channel)');
assert.equal(effects[0].channel, 'cyber_disruption');
});
it('blocks Brazil → Israel conflict via External Power Broker (security_escalation, spec 0.85 < 0.90)', () => {
const effects = buildCrossSituationEffects({
situationSimulations: [
makeSimulation('sit-brazil', 'Brazil conflict situation', 'conflict', ['Brazil'], 'escalatory', 0.84,
[{ type: 'security_escalation', count: 3 }]),
makeSimulation('sit-israel', 'Israel conflict situation', 'conflict', ['Israel'], 'escalatory', 0.88),
],
reportableInteractionLedger: [
makeInteraction('sit-brazil', 'Brazil conflict situation', 'sit-israel', 'Israel conflict situation',
'security_escalation', 'round_1', 5.8, 0.87, 0.85, true, false),
makeInteraction('sit-brazil', 'Brazil conflict situation', 'sit-israel', 'Israel conflict situation',
'security_escalation', 'round_2', 5.7, 0.86, 0.85, true, false),
],
});
assert.equal(effects.length, 0, 'Brazil → Israel via generic external actor should be blocked (actorSpec 0.85 < 0.90)');
});
it('allows Brazil → Mexico (same macro-region, security_escalation → infrastructure)', () => {
const effects = buildCrossSituationEffects({
situationSimulations: [
makeSimulation('sit-brazil', 'Brazil conflict situation', 'conflict', ['Brazil'], 'escalatory', 0.84,
[{ type: 'security_escalation', count: 3 }]),
// target must be infrastructure for security_escalation:infrastructure relation to exist
makeSimulation('sit-mexico', 'Mexico infrastructure situation', 'infrastructure', ['Mexico'], 'escalatory', 0.72),
],
reportableInteractionLedger: [
makeInteraction('sit-brazil', 'Brazil conflict situation', 'sit-mexico', 'Mexico infrastructure situation',
'security_escalation', 'round_1', 5.8, 0.87, 0.85, true, false),
makeInteraction('sit-brazil', 'Brazil conflict situation', 'sit-mexico', 'Mexico infrastructure situation',
'security_escalation', 'round_2', 5.7, 0.86, 0.85, true, false),
],
});
assert.equal(effects.length, 1, 'Brazil → Mexico should pass (both AMERICAS, cross-theater gate does not apply)');
assert.equal(effects[0].channel, 'security_escalation');
});
it('blocks Cuba → Iran infrastructure (cross-theater, service_disruption, spec 0.73 < 0.90)', () => {
const effects = buildCrossSituationEffects({
situationSimulations: [
makeSimulation('sit-cuba', 'Cuba infrastructure situation', 'infrastructure', ['Cuba'], 'contested', 0.62,
[{ type: 'service_disruption', count: 3 }]),
makeSimulation('sit-iran', 'Iran infrastructure situation', 'infrastructure', ['Iran'], 'contested', 0.58),
],
reportableInteractionLedger: [
makeInteraction('sit-cuba', 'Cuba infrastructure situation', 'sit-iran', 'Iran infrastructure situation',
'service_disruption', 'round_1', 5.5, 0.84, 0.73, true, false),
makeInteraction('sit-cuba', 'Cuba infrastructure situation', 'sit-iran', 'Iran infrastructure situation',
'service_disruption', 'round_2', 5.4, 0.83, 0.73, true, false),
],
});
assert.equal(effects.length, 0, 'Cuba → Iran via generic civil-protection actor should be blocked');
});
});