Skip to content

Phase 0: Regional Intelligence snapshot writer foundation#2940

Merged
koala73 merged 7 commits into
mainfrom
worktree-compiled-weaving-riddle
Apr 11, 2026
Merged

Phase 0: Regional Intelligence snapshot writer foundation#2940
koala73 merged 7 commits into
mainfrom
worktree-compiled-weaving-riddle

Conversation

@koala73

@koala73 koala73 commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 0 of the Regional Intelligence Model. Foundation only - no UI changes, no PRO panel, no LLM calls. This PR ships the canonical data contract, the deterministic compute pipeline, and the persistence/idempotency layer that every subsequent phase reads from.

  • New shared/geography.js defines 8 display regions, 19 theaters, 11 corridors with chokepoint links, country and corridor criticality weights matching the scoring appendix.
  • New shared/regions.types.d.ts is the type contract for RegionalSnapshot and every nested message (BalanceVector 7-axis split, ScenarioSet per horizon, TriggerThreshold structured operator+baseline, ActorState/LeverageEdge, typed TransmissionPath with template provenance, MobilityState, NarrativeSection, SnapshotDiff). Mirrors the proto shape that Phase 1 codegens.
  • New shared/iso2-to-region.json (218 codes from World Bank API) with strategic overrides documented inline: AF/PK to south-asia, TR to mena, MX to north-america (USMCA frame), TW to east-asia.
  • New scripts/regional-snapshot/ modules implement the canonical compute pipeline: pre-meta -> balance -> actors -> triggers (BEFORE scenarios) -> normalized scenarios -> transmissions -> evidence -> snapshot_id -> diff -> final_meta. No LLM in Phase 0.
  • New scripts/seed-regional-snapshots.mjs entry point with isMain guard. Persists atomically via SET NX EX for idempotency, prunes the index on every persist (ZREMRANGEBYSCORE), busts the optional :live cache.
  • Wired into seed-bundle-derived-signals at 6h interval. The bundle cron fires every 5min; the freshness gate ensures the snapshot only runs when seed-meta is >4.8h old, sharing the existing service slot.
  • New tests/regional-snapshot.test.mjs with 50 unit tests covering geography invariants, balance vector formulas, weighted-tail amplification, regime rules, trigger gating, scenario normalization sums, transmission template matching, snapshot meta merging, diff engine for every alert type, and end-to-end across all 8 regions.

Spec

Architecture: docs/internal/pro-regional-intelligence-upgrade.md
Engineering appendix: docs/internal/pro-regional-intelligence-appendix-engineering.md
Scoring appendix: docs/internal/pro-regional-intelligence-appendix-scoring.md

Testing

  • npm run test:data - 3950 tests pass (the new 50 + the existing 3900)
  • npm run typecheck:all - clean
  • npx biome lint - 0 warnings on the new files
  • End-to-end smoke test runs the full pipeline against mock data for all 8 regions without throwing

What this PR does NOT include

  • LLM narrative generation (Phase 1)
  • The RegionalIntelligenceBoard panel and proto/RPC handlers (Phase 1)
  • Server-side get-regional-snapshot RPC (Phase 1)
  • State-change alerts via the diff engine (Phase 2)
  • Mobility lane population - schema reserves the fields, Phase 2 wires the inputs
  • Forecast region filter UI quick win (separate PR2 - independent)

Post-Deploy Monitoring & Validation

  • What to monitor/search
    • Logs: Railway service seed-bundle-derived-signals logs for [Regional-Snapshots] lines. Expect one persist entry per region (8 lines) every 6 hours.
    • Metrics/Dashboards: /api/health endpoint should show regionalSnapshots as OK once the first run lands (within 6h of merge).
  • Validation checks
    • curl https://worldmonitor.app/api/health | jq '.checks.regionalSnapshots' should return ok status with a recent fetchedAt timestamp.
    • Inspect Redis: GET intelligence:snapshot:v1:mena:latest returns a snapshot_id, then GET intelligence:snapshot-by-id:v1:{snapshotId} returns the full snapshot JSON.
    • ZCARD intelligence:snapshot-index:v1:mena should be >= 1 after the first run.
  • Expected healthy behavior
    • All 8 regions persisted on each run
    • snapshot_confidence between 0.5 and 1.0 (lower if many input keys are missing or stale)
    • regime defaults to calm for low-activity regions, more interesting labels for active regions
    • Trigger ladders populated with active/watching/dormant per region
  • Failure signal(s) / rollback trigger
    • Health check shows regionalSnapshots red (>720min stale): check Railway logs for failed runs, then revert this PR if the bundle service crashes
    • All persists return duplicate-bucket: dedup logic inverted, revert
    • snapshot_confidence 0 across all regions: input pipeline broken, investigate Redis credentials
  • Validation window & owner
    • Window: first 24 hours after merge (4 cron cycles)
    • Owner: @koala73 (PR author monitors initial cycles)

Test plan

  • Merge to main
  • Wait for one bundle cycle (~5 min) - verify Railway logs show [Regional-Snapshots] Skipped (because no prior seed-meta exists yet, the first run should actually fire)
  • After first run: verify seed-meta:intelligence:regional-snapshots exists in Redis
  • Verify 8 snapshots persisted: one latest pointer per region
  • Confirm /api/health endpoint includes regionalSnapshots in OK status
  • No errors in Railway logs from the existing Correlation or Cross-Source-Signals seeds (Phase 0 must not break the bundle's existing members)

koala73 added 3 commits April 11, 2026 13:20
…napshot types

Adds shared/ foundation for the Regional Intelligence Model:

  - shared/regions.types.d.ts: type contract for RegionalSnapshot, BalanceVector
    (7 axes, pressures vs buffers split), ScenarioSet (normalized per horizon),
    TriggerThreshold (structured operator+baseline), ActorState/LeverageEdge,
    TransmissionPath (typed magnitude/latency/asset class with template
    provenance), MobilityState, NarrativeSection, SnapshotDiff, SnapshotMeta
    (snapshot_id + freshness tracking).

  - shared/iso2-to-region.json: 218 ISO2 codes mapped to 8 regions, generated
    from World Bank /v2/country with strategic overrides documented inline:
      AF/PK -> south-asia (WB has them in MEA)
      TR    -> mena       (WB has it in ECS)
      MX    -> north-america (WB has it in LCN, USMCA frame)
      TW    -> east-asia  (WB does not list Taiwan)

  - shared/geography.js: 8 regions, 19 theaters, 11 corridors with chokepoint
    links. Country and corridor criticality weights matching the scoring
    appendix. Helper functions: getRegion, regionForCountry, getRegionCorridors,
    countryCriticality.

  - scripts/shared/iso2-to-region.json: mirror per the existing convention
    enforced by tests/edge-functions.test.mjs.

Spec: docs/internal/pro-regional-intelligence-upgrade.md
…te pipeline

Adds the Phase 0 Regional Intelligence snapshot writer. No LLM narrative call;
narrative arrives in Phase 1. All scoring is deterministic.

Compute pipeline (canonical order, mirrors spec):

  1. read raw inputs (parallel Redis pipeline)
  2. build pre_meta (versions, freshness, missing/stale, valid_until)
  3. compute 7-axis balance vector with weighted-tail domestic fragility
  4. score actors and leverage edges from forecast case files
  5. evaluate structured trigger thresholds (BEFORE scenarios)
  6. build scenario sets normalized to 1.0 per horizon
  7. resolve transmission templates against active triggers
  8. mobility (empty in v1, see appendix)
  9. gather and attribute evidence chain
  10. SKIPPED: narrative (Phase 1)
  11. assign UUID v7 snapshot_id
  12. read previous snapshot, run diffRegionalSnapshot
  13. build final_meta with diff-derived trigger_reason
  14. persist atomically (SET NX EX dedup, ZADD index, ZREMRANGEBYSCORE prune,
      DEL optional :live)
  15. (alerts emitted from diff in Phase 2)

Modules under scripts/regional-snapshot/:
  _helpers, freshness (registry of source max-age),
  balance-vector (per-axis formulas from scoring appendix, weighted-tail
  fragility, deterministic energy split into vulnerability vs leverage),
  regime-derivation (rule table), actor-scoring,
  trigger-evaluator + triggers.config (delta operators dormant in Phase 0),
  scenario-builder (per-horizon normalization), transmission-templates
  (~7 templates with template_id + template_version),
  evidence-collector, snapshot-meta (pre/final split),
  diff-snapshot (single source of truth for state changes),
  persist-snapshot (atomic SET NX EX for idempotency, prune on every persist).

Entry point scripts/seed-regional-snapshots.mjs:
  - isMain guard so the bundle runner can spawn it as a child process
  - Reads all inputs once, then iterates 8 regions
  - Writes seed-meta:intelligence:regional-snapshots after the loop
  - Exits non-zero only if every region failed

Tests in tests/regional-snapshot.test.mjs (50 tests, all passing):
  geography invariants, helpers, balance vector axes and decomposition,
  weighted-tail amplification, regime rules, trigger gating, scenario
  normalization sums, transmission template matching, snapshot meta
  pre/final merge, diff engine for every alert type, end-to-end across all
  8 regions.

Spec: docs/internal/pro-regional-intelligence-upgrade.md
…y 6h

  - scripts/seed-bundle-derived-signals.mjs: add Regional-Snapshots section
    (6h interval, 180s timeout). Bundle cron fires every 5min; the freshness
    gate ensures the snapshot script only runs when seed-meta is >4.8h old,
    so the effective cadence is 6h while sharing the existing service slot.

  - api/health.js SEED_META: add regionalSnapshots entry with maxStaleMin 720
    (12h = 2x cron interval). Health check goes red if no snapshot has been
    persisted in the last 12 hours.
@vercel

vercel Bot commented Apr 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Apr 11, 2026 0:50am

Request Review

@greptile-apps

greptile-apps Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR ships the Phase 0 foundation for the Regional Intelligence Model: geography taxonomy, deterministic compute pipeline (balance vector → regime → actors → triggers → scenarios → transmissions → evidence → diff), Redis persistence with SET NX idempotency, and health-check wiring. The architecture is clean and the pipeline ordering is sound, but one input key version mismatch in the freshness registry will silently degrade every snapshot's snapshot_confidence from day one.

  • freshness.mjs line 30 references intelligence:advisories-bootstrap:v2, but the seeder and every other consumer in the codebase use v1. This key is never written, so it will always appear in missing_inputs and permanently shave ~7 % off snapshot_confidence.

Confidence Score: 4/5

Safe to merge after fixing the advisories key version; all other findings are P2 style/future-proofing.

One P1 defect in freshness.mjs references a Redis key (advisories-bootstrap:v2) that is never written, permanently understating snapshot_confidence. The fix is a one-character change. All remaining comments are P2.

scripts/regional-snapshot/freshness.mjs (wrong key version at line 30)

Important Files Changed

Filename Overview
scripts/regional-snapshot/freshness.mjs Freshness registry references non-existent advisories-bootstrap:v2 key (actual key is v1), causing permanent false missing-input classification.
scripts/seed-regional-snapshots.mjs Pipeline ordering and idempotency wiring look correct; returned snapshot includes undeclared diff field that is persisted to Redis but absent from the RegionalSnapshot type.
scripts/regional-snapshot/persist-snapshot.mjs Atomic SET NX idempotency guard, sorted-set index with 90-day prune, and latest pointer look correct. Minor: diff field from caller leaks into the persisted JSON shape.
scripts/regional-snapshot/trigger-evaluator.mjs Trigger state machine is correct for current gt/gte operators; isCloseToThreshold silently breaks for lt/lte operators that may be added later.
scripts/regional-snapshot/balance-vector.mjs Seven-axis deterministic scoring with correct clip/weighted-average/percentile helpers; Phase 0 proxy stubs documented.
scripts/regional-snapshot/diff-snapshot.mjs Diff engine covers all five alert types; Phase 0 omissions clearly annotated.
shared/regions.types.d.ts Complete type contract for RegionalSnapshot and nested shapes; missing optional diff?: SnapshotDiff field that the seeder attaches before persisting.
api/health.js New regionalSnapshots seed-meta entry added with 720-min staleness threshold (2× 6h cadence), matching the seed-meta key written by the seeder.
scripts/seed-bundle-derived-signals.mjs Regional-Snapshots correctly wired into derived-signals bundle at 6h interval with 180s timeout.
scripts/regional-snapshot/scenario-builder.mjs Lane building and normalization logic is correct; matchesHorizon has harmless duplicate regex tokens.

Reviews (1): Last reviewed commit: "chore(seed-bundle): wire regional-snapsh..." | Re-trigger Greptile

Comment thread scripts/regional-snapshot/freshness.mjs Outdated
{ key: 'economic:eu-gas-storage:v1', maxAgeMin: 2880, feedsAxes: ['energy_vulnerability'] },
{ key: 'economic:spr:v1', maxAgeMin: 10080, feedsAxes: ['energy_buffer'] },
{ key: 'energy:chokepoint-flows:v1', maxAgeMin: 240, feedsAxes: ['transmission'] },
{ key: 'intelligence:advisories-bootstrap:v2', maxAgeMin: 240, feedsAxes: ['mobility', 'evidence'] },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Wrong advisories key version — permanently missing input

The key here is intelligence:advisories-bootstrap:v2, but every other reference in the codebase uses v1 — the seed script (seed-security-advisories.mjs), health check (api/health.js), api/bootstrap.js, server/_shared/cache-keys.ts, and seed-cross-source-signals.mjs all write and read v1. Because v2 is never written to Redis, classifyInputs will always classify this entry as missing, permanently reducing snapshot_confidence by roughly 6–7 % and polluting missing_inputs on every snapshot with this key.

Change v2 to v1 to match the actual key written by the advisories seeder.

Comment on lines +120 to +126
function isCloseToThreshold(value, threshold) {
// 80% of the threshold counts as "watching"
const target = threshold.value;
if (target === 0) return false;
const ratio = value / target;
return ratio > 0.8 && ratio < 1.0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 isCloseToThreshold never fires for lt / lte operators

When evaluateThreshold returns false for a lt threshold, the caller already guarantees value >= target, meaning ratio = value / target >= 1.0. The condition ratio > 0.8 && ratio < 1.0 can never be true, so the watching state is unreachable for any lt/lte trigger — they jump directly from dormant to active. No current triggers use these operators, so there's no runtime impact today, but future phases may add them.

Comment on lines +74 to +77
if (targetHorizon === '24h') return /h24|24h|day|24h/.test(forecastHorizon);
if (targetHorizon === '7d') return /d7|7d|week|d7/.test(forecastHorizon);
if (targetHorizon === '30d') return /d30|30d|month|d30/.test(forecastHorizon);
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate regex tokens in matchesHorizon

Each pattern contains a duplicated alternative — 24h appears twice in the first regex, d7 twice in the second, d30 twice in the third. The duplicates are harmless but suggest four distinct patterns when there are only three.

Comment thread scripts/seed-regional-snapshots.mjs Outdated
trigger_reason: triggerReason,
});

return { ...tentativeSnapshot, meta: finalMeta, diff };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 diff persisted in snapshot JSON but absent from the RegionalSnapshot type

The spread here attaches diff: SnapshotDiff to the object before it is serialised to Redis, but RegionalSnapshot in regions.types.d.ts has no diff field. Consumers that deserialise via readLatestSnapshot will hold an undeclared field that may confuse Phase 1 codegen. Either add diff?: SnapshotDiff to the interface, or strip diff before persisting.

koala73 added 2 commits April 11, 2026 14:22
…riter

Resolves all 5 critical findings from the multi-agent code review of PR #2940.
End-to-end smoke test passes for all 8 regions with confidence=1.0.

P1 #166: health-seed-meta-not-in-keys-loops
  - Add regionalSnapshots to STANDALONE_KEYS in api/health.js
  - Without this, the SEED_META entry was dead wiring and the 12h staleness
    budget was unobservable. Same failure mode as the 'empty data ok keys
    bootstrap blind spot' the team has hit before.

P1 #167: oref-trigger-key-not-in-freshness-registry
  - Add relay:oref:history:v1 to FRESHNESS_REGISTRY (the canonical OREF key
    written by ais-relay, not the wrong intelligence:oref-alerts:v1 that the
    code was originally reading).
  - Update trigger-evaluator to read activeAlertCount/historyCount24h from
    the actual relay:oref:history:v1 payload shape.
  - oref_cluster trigger now fires when activeAlertCount > 10 (verified
    end-to-end against mock data).

P1 #168: zombie-freshness-registry-keys
  - Remove 4 freshness registry entries that no compute module reads:
    supply_chain:shipping_stress, energy:chokepoint-flows,
    intelligence:advisories-bootstrap, market:commodities-bootstrap.
  - These were dragging snapshot_confidence below 1.0 and wasting Redis
    pipeline reads. Add a header comment forbidding speculative entries.

P1 #169: diff-field-leaks-into-persisted-snapshot
  - Change computeSnapshot to return { snapshot, diff } separately so the
    diff is consumed locally for inferTriggerReason and never serialized
    into the persisted snapshot. The diff field was not part of the
    RegionalSnapshot type and would have broken Phase 1 proto codegen.
  - Update main() to destructure { snapshot } before persistSnapshot.

P1 #170: jsdoc-types-not-enforced-jsconfig-missing
  - Add scripts/seed-regional-snapshots.mjs and scripts/regional-snapshot/**
    to scripts/jsconfig.json's include array so tsc --checkJs validates the
    JSDoc @type annotations.
  - Add // @ts-check directive to all 14 .mjs files in the regional-snapshot
    pipeline.
  - Fix 4 type-safety bugs surfaced by enabling type-checking:
      actor-scoring: explicit ActorState[] type on local actors array; cast
        leverage_domains to ActorLeverageDomain[]; typed ActorRole return on
        inferRole.
      evidence-collector: explicit EvidenceItem[] type on local out array.
      scenario-builder: typed HORIZONS as ScenarioHorizon[] and LANE_NAMES
        as ScenarioName[].
      transmission-templates: cast tpl.affectedRegions to RegionId[] when
        building TransmissionPath objects.
  - Pre-existing seed-forecasts.mjs and _r2-storage.mjs errors (46 total)
    are not part of this PR's scope and remain untouched.

Verification
  - npx tsc --noEmit -p scripts/jsconfig.json: 0 errors in regional-snapshot files
  - npm run typecheck:all: clean
  - npm run test:data: 3946/3946 pass
  - tests/regional-snapshot.test.mjs: 50/50 pass
  - End-to-end smoke test: all 8 regions compute clean with confidence=1.0
  - Verified: oref_cluster trigger fires when activeAlertCount > 10
  - Verified: persisted snapshot has no diff field
… review

Records 19 P2 and 3 P3 todos from the kieran-typescript, security-sentinel,
performance-oracle, architecture-strategist, code-simplicity, agent-native,
and learnings researcher reviews of PR #2940 and PR #2942.

P2 (19): isCloseToThreshold inverted for lt; sequential readLatestSnapshot
1600ms; sequential persist 1600ms; seeder bypasses runSeed; region taxonomy
3 sources of truth; Redis keys coupled to compute modules; stored XSS risk;
AbortController missing; getForecasts no cachedFetchJson; REGIONS.find
duplicated; undated inputs treated as fresh; inconsistent unknown-region
error handling; writeExtraKeyWithMeta positional args; pipeline non-atomic
persist; trigger watching runs on delta operators; regime.transition_driver
always empty; orphan scripts/shared mirror; dangling docs/internal refs;
refetchForRegion silent error swallowing.

P3 (3): redundant JSON.stringify(caseFile) hot loops; helper/config
cleanups (round dedup, generateSnapshotId entropy, matchesHorizon regex,
coercive_pressure dead 0.30 weight); perf micro-cleanups (buildPreMeta x8,
signal indexing, prototype pollution guards).
@koala73

koala73 commented Apr 11, 2026

Copy link
Copy Markdown
Owner Author

P1 review findings addressed (5/5)

All 5 P1 findings from the multi-agent code review have been fixed in commits 3f449f3 and d322b25. The 22 remaining P2/P3 findings are tracked as pending todos in todos/.

What changed

# Issue Fix
166 Health SEED_META not in STANDALONE_KEYS or BOOTSTRAP_KEYS (dead wiring) Added regionalSnapshots: 'intelligence:regional-snapshots:summary:v1' to STANDALONE_KEYS in api/health.js.
167 OREF cluster trigger reads key not in FRESHNESS_REGISTRY (silently dormant) Added relay:oref:history:v1 (the canonical OREF key from ais-relay) to FRESHNESS_REGISTRY. Updated trigger-evaluator.mjs to read activeAlertCount / historyCount24h from the actual payload shape.
168 Four zombie freshness registry keys with no compute consumer Removed supply_chain:shipping_stress:v1, energy:chokepoint-flows:v1, intelligence:advisories-bootstrap:v2, market:commodities-bootstrap:v1. Added a header comment forbidding speculative entries.
169 diff field serialized into persisted snapshot (proto contract leak) Changed computeSnapshot to return { snapshot, diff } separately. Persist only snapshot. The diff is consumed locally for inferTriggerReason.
170 JSDoc @type annotations not validated by tsc --checkJs Added scripts/seed-regional-snapshots.mjs and scripts/regional-snapshot/**/*.mjs to scripts/jsconfig.json include. Added // @ts-check directive to all 14 .mjs files. Fixed 4 type-safety bugs surfaced by enabling type-checking (typed local arrays in actor-scoring/evidence-collector, typed const arrays in scenario-builder, typed cast in transmission-templates).

Verification

Check Result
npx tsc --noEmit -p scripts/jsconfig.json (regional-snapshot files) 0 errors
npm run typecheck:all clean
npm run test:data 3946 / 3946 pass
tests/regional-snapshot.test.mjs 50 / 50 pass
End-to-end smoke test (8 regions, mock data) All compute clean, confidence=1.0
oref_cluster trigger fires when activeAlertCount > 10 Verified
Persisted snapshot has no diff field Verified

Pre-existing tech debt (NOT addressed in this PR)

scripts/jsconfig.json now type-checks the regional-snapshot pipeline. As a side effect it surfaces 46 pre-existing type errors in seed-forecasts.mjs (44) and _r2-storage.mjs (2). Those files were already in the include array before this PR, the errors were not caught earlier because nothing in CI runs tsc --noEmit -p scripts/jsconfig.json directly. They are tech debt for a separate cleanup PR.

Open todos (22 P2/P3, tracked in todos/)

  • 19 P2 (perf, correctness, architecture, security), see commit d322b25 for the full list
  • 3 P3 (cleanups and micro-optimizations)

These are recommended to address before Phase 1, but none block Phase 0 merge.

…tor and seed exit

Fixes wrong payload shapes in computeCapitalStress/computeEnergyVulnerability
and silent partial-failure handling in the seed entry point.

P1: computeCapitalStress wired to wrong production shapes and units

  - economic:macro-signals:v1 emits verdict 'BUY' | 'CASH' | 'UNKNOWN', not
    'BEARISH' | 'NEUTRAL' | 'BULLISH' (seed-economy.mjs:497). Map CASH=1,
    BUY=0, UNKNOWN=0.5.
  - economic:national-debt:v1 shape is { entries: [{ iso3, debtToGdp, ... }] },
    not { countries: [{ country, debtToGdp }] } (seed-national-debt.mjs:101).
    Filter via iso3 -> iso2 lookup (shared/iso3-to-iso2.json) to match
    region countries.
  - debtToGdp is a PERCENTAGE (e.g., 123 for 123%), not a 0-1 fraction.
    Old code computed (avgDebt - 0.6) / 0.8 assuming fraction; that
    saturated every non-zero value to 1.0. New code uses
    (avgDebtPct - 60) / 80 so 60% is neutral and 140% saturates.
  - economic:stress-index:v1 is a SINGLE GLOBAL object with
    { compositeScore: 0-100, label, components } from seed-economy.mjs
    computeStressIndex(), NOT a per-country table. Apply as a global
    overlay that scales every region's capital_stress equally (the FRED
    composite primarily reflects US financial conditions, which do affect
    every region).

P2: computeEnergyVulnerability read wrong field and wrong scale

  - energy:mix:v1:_all stores importShare (OWID percentage 0-100), not
    imported (seed-owid-energy-mix.mjs:193). Old code read m.imported
    which is undefined, producing avgImport = 0 always.
  - Divide by 100 to normalize percentage to 0-1 fraction before clipping.
  - Skip countries with null importShare (exclude from average, don't
    treat as zero). Report the valid-count in the driver description so
    the evidence chain reflects actual coverage.

P1: partial region failures silently treated as successful run

  - Old exit condition (failed > 0 && persisted === 0) meant 7 successful
    + 1 failed region wrote fresh seed-meta and returned exit 0. Health
    check stayed OK and the bundle runner never retried.
  - New behavior: only write seed-meta when ALL regions succeed. On any
    failure, log the failedRegions array with error messages, skip the
    seed-meta write, and exit 1. The 12h maxStaleMin means /api/health
    will flip to STALE after two consecutive degraded runs, and the
    bundle runner freshness gate will retry on the next 5-min cycle.
  - Track failedRegions for operator observability in the log tail.

Verification
  - tests/regional-snapshot.test.mjs: 50/50 pass
  - npm run test:data: 3950/3950 pass
  - npm run typecheck:all: clean
  - npx tsc --noEmit -p scripts/jsconfig.json: 0 errors in regional-snapshot
  - biome lint: clean on modified files
  - End-to-end smoke test with production payload shapes verifies:
    - CASH verdict drives cMacro=1.0 (MENA capital_stress=0.454 matches formula)
    - Debt 123%/107% drives cDebt=0.688 (N.America capital_stress=0.592)
    - Stress compositeScore=68 drives cStress=0.68 (global overlay)
    - importShare 85% normalizes to 0.85 (MENA energy_vulnerability=0.197)
    - importShare null excluded from average, not zeroed
@koala73

koala73 commented Apr 11, 2026

Copy link
Copy Markdown
Owner Author

Review round 2 findings addressed (2 P1 + 1 P2)

Fixes pushed in commit c92eb12 address all 3 findings from the round-2 review.

What changed

Finding Fix
P1: computeCapitalStress wired to wrong production shapes Fixed macro verdict mapping (BUY/CASH/UNKNOWN from seed-economy.mjs:497), fixed national-debt shape ({entries: [{iso3, debtToGdp}]} from seed-national-debt.mjs:101), added iso3→iso2 lookup via shared/iso3-to-iso2.json, fixed debt percentage math (60% neutral / 140% saturation, not 0.6/0.8), fixed stress-index as global overlay (compositeScore: 0-100, not per-country table).
P1: partial region failures silently treated as successful Old exit condition failed > 0 && persisted === 0 meant 7-of-8 successful runs still wrote fresh seed-meta and exited 0. New behavior: only write seed-meta when ALL regions succeed; skip meta + exit 1 on any failure. The 12h maxStaleMin flips /api/health to STALE after two consecutive degraded runs. Track failedRegions[] for operator observability.
P2: computeEnergyVulnerability wrong field + wrong scale Field is importShare not imported (per seed-owid-energy-mix.mjs:193). Values are OWID percentages (0-100), not 0-1 fractions. Divide by 100 before clipping. Skip countries with null importShare (exclude from average, don't treat as zero).

Verification

End-to-end smoke test with actual production payload shapes confirms the math:

Region Axis Result Expected
MENA capital_stress 0.454 0.25·1.0 (CASH) + 0.20·0 (debt avg 37.5% < 60%) + 0.30·0.68 (stress 68/100) = 0.454
N. America capital_stress 0.592 0.25·1.0 + 0.20·0.688 (debt avg 115%, (115-60)/80) + 0.30·0.68 = 0.592
MENA energy_vulnerability 0.197 0.5·0.3325 (importShare avg 33.25%) + 0.25·0.1 (EU gas 72%) + 0.25·0.022 (SPR 88d) = 0.197

All assertions pass with predicted numerical matches.

Test coverage

  • tests/regional-snapshot.test.mjs: 50/50 pass
  • npm run test:data: 3950/3950 pass
  • npm run typecheck:all: clean
  • npx tsc --noEmit -p scripts/jsconfig.json: 0 errors in regional-snapshot files
  • biome lint: clean on modified files
  • End-to-end smoke test validates all 8 regions against real seed shapes

Follow-up considerations

The global stress-index overlay is a simplification. The FRED composite primarily reflects US/global financial conditions. Phase 1 could introduce regional overlays (e.g., EU financial stress, EM sovereign spreads) and weight them per region. For Phase 0, the global overlay is acceptable since every region is affected by US conditions.

…skipped

Addresses 1 P1 finding on PR #2940: the seed script was overwriting a
good summary with an empty {regions: [], recordCount: 0} payload
whenever a cron invocation landed inside the 15-minute idempotency
bucket and every region returned duplicate-bucket. api/health.js
classifies the empty array as EMPTY_DATA, flipping /api/health to red
on a completely harmless retry.

New health policy in the seed main():
  1. persisted > 0 && failed === 0: write fresh summary + seed-meta
  2. persisted === 0 && failed === 0: all regions dedup-skipped. Do
     NOT write (preserve the prior good summary). Return cleanly.
  3. failed > 0: skip meta write and exit 1. The 12h maxStaleMin
     budget flips /api/health to STALE on sustained degradation.

The bundle runner's freshness gate (intervalMs * 0.8) means the next
full run lands roughly every 6h regardless. In steady state, the 15min
dedup bucket only catches bundle cron retries after a prior success
within the same bucket, so path 2 is rare but real: the first cron
tick after a pod restart inside the dedup window is the failure mode
this fixes.

Verification
  - tests/regional-snapshot.test.mjs: 50/50 pass
  - npm run typecheck:all: clean
  - tsc -p scripts/jsconfig.json: 0 errors in regional-snapshot
  - biome lint: clean on touched file
  - Static analysis confirms exactly 1 writeExtraKeyWithMeta call
    (only in branch 1) so branches 2 and 3 cannot accidentally
    overwrite the summary
@koala73

koala73 commented Apr 11, 2026

Copy link
Copy Markdown
Owner Author

Review round 3 — 1 P1 addressed

Fix in commit bef3225.

Problem

scripts/seed-regional-snapshots.mjs:224 was writing a fresh intelligence:regional-snapshots:summary:v1 payload whenever failed === 0, including the case where every region returned duplicate-bucket from the 15-minute idempotency guard. That produced an empty {regions: [], generatedAt: ...} with recordCount: 0. api/health.js:226 and :448 classify empty arrays as EMPTY_DATA, flipping regionalSnapshots health red on a completely harmless retry.

Fix

New three-branch health policy in main():

Branch Condition Action
1 persisted > 0 && failed === 0 Write fresh summary + seed-meta
2 persisted === 0 && failed === 0 All dedup-skipped. Preserve prior summary. No write. Return cleanly.
3 failed > 0 Skip write, log failed regions, exit 1

In steady state, the 15-minute dedup bucket rarely traps a full run because the bundle runner's freshness gate (intervalMs * 0.8) already spaces invocations ~5 hours apart. The failure mode this fixes is the first cron tick after a Railway pod restart that lands inside the dedup window of the prior successful run.

Verification

  • tests/regional-snapshot.test.mjs: 50/50 pass
  • npm run typecheck:all: clean
  • tsc -p scripts/jsconfig.json: 0 errors in regional-snapshot
  • biome lint: clean on modified file
  • Static analysis confirms only 1 writeExtraKeyWithMeta call in the file (inside branch 1), so branches 2 and 3 cannot accidentally overwrite.

@koala73
koala73 merged commit 7dfdc81 into main Apr 11, 2026
11 checks passed
koala73 added a commit that referenced this pull request Apr 11, 2026
Two review findings on PR #2940 caused MENA/SSA snapshots to silently
drop broad cross-source signals and leak foreign chokepoint events into
every region's evidence chain.

P1 - theater label matching
  seed-cross-source-signals.mjs normalizes raw values to broad display
  labels like "Middle East" and "Sub-Saharan Africa". The consumer side
  compared these against region.theaters kebab IDs (levant, persian-gulf,
  horn-of-africa). "middle east" does not substring-match any of those,
  so every MENA signal emitted with the broad label was silently dropped
  from coercive_pressure and the evidence chain. Same for SSA.

  Added signalAliases per region and a shared isSignalInRegion helper in
  shared/geography.js. Both balance-vector.mjs and evidence-collector.mjs
  now route signals through the helper, which normalizes both sides and
  matches against theater IDs or region aliases.

P2 - chokepoint region leak
  evidence-collector.mjs:62 iterated every chokepoint in the payload
  without filtering by regionId, so Taiwan Strait, Baltic, and Panama
  threat events surfaced in MENA and SSA evidence chains. Now derives
  the allowed chokepoint ID set from getRegionCorridors(regionId) and
  skips anything not owned by the region.

Added 15 unit tests covering: broad-label matching, kebab/spaced input,
cross-region rejection, and the chokepoint filter for MENA/East Asia/
Europe/SSA.
koala73 added a commit that referenced this pull request Apr 11, 2026
Two review findings on PR #2940 caused MENA/SSA snapshots to silently
drop broad cross-source signals and leak foreign chokepoint events into
every region's evidence chain.

P1 - theater label matching
  seed-cross-source-signals.mjs normalizes raw values to broad display
  labels like "Middle East" and "Sub-Saharan Africa". The consumer side
  compared these against region.theaters kebab IDs (levant, persian-gulf,
  horn-of-africa). "middle east" does not substring-match any of those,
  so every MENA signal emitted with the broad label was silently dropped
  from coercive_pressure and the evidence chain. Same for SSA.

  Added signalAliases per region and a shared isSignalInRegion helper in
  shared/geography.js. Both balance-vector.mjs and evidence-collector.mjs
  now route signals through the helper, which normalizes both sides and
  matches against theater IDs or region aliases.

P2 - chokepoint region leak
  evidence-collector.mjs:62 iterated every chokepoint in the payload
  without filtering by regionId, so Taiwan Strait, Baltic, and Panama
  threat events surfaced in MENA and SSA evidence chains. Now derives
  the allowed chokepoint ID set from getRegionCorridors(regionId) and
  skips anything not owned by the region.

Added 15 unit tests covering: broad-label matching, kebab/spaced input,
cross-region rejection, and the chokepoint filter for MENA/East Asia/
Europe/SSA.
koala73 added a commit that referenced this pull request Apr 11, 2026
* fix(intelligence): region-scope signals and chokepoint evidence

Two review findings on PR #2940 caused MENA/SSA snapshots to silently
drop broad cross-source signals and leak foreign chokepoint events into
every region's evidence chain.

P1 - theater label matching
  seed-cross-source-signals.mjs normalizes raw values to broad display
  labels like "Middle East" and "Sub-Saharan Africa". The consumer side
  compared these against region.theaters kebab IDs (levant, persian-gulf,
  horn-of-africa). "middle east" does not substring-match any of those,
  so every MENA signal emitted with the broad label was silently dropped
  from coercive_pressure and the evidence chain. Same for SSA.

  Added signalAliases per region and a shared isSignalInRegion helper in
  shared/geography.js. Both balance-vector.mjs and evidence-collector.mjs
  now route signals through the helper, which normalizes both sides and
  matches against theater IDs or region aliases.

P2 - chokepoint region leak
  evidence-collector.mjs:62 iterated every chokepoint in the payload
  without filtering by regionId, so Taiwan Strait, Baltic, and Panama
  threat events surfaced in MENA and SSA evidence chains. Now derives
  the allowed chokepoint ID set from getRegionCorridors(regionId) and
  skips anything not owned by the region.

Added 15 unit tests covering: broad-label matching, kebab/spaced input,
cross-region rejection, and the chokepoint filter for MENA/East Asia/
Europe/SSA.

* fix(intelligence): address Greptile P2 review findings on #2952

Two P2 findings from Greptile on the region-scoping PR.

1) Drop 'eu' short alias from europe.signalAliases
   `isSignalInRegion` uses substring matching, and bare 'eu' would match
   any theater label containing those two letters ('fuel', 'neutral zone',
   'feudal'). Replaced with 'european union' which is long enough to be
   unambiguous. No seed currently emits a bare 'eu' label, so this is
   pure hardening.

2) Make THEATERS.corridorIds live data via getRegionCorridors union
   horn-of-africa declared corridorIds: ['babelm'] and caribbean declared
   corridorIds: ['panama'], but `getRegionCorridors` only consulted
   CORRIDORS.theaterId — so those entries were dead data. After yesterday's
   region-scoped chokepoint filter, Bab el-Mandeb threat events landed
   ONLY in MENA evidence (via the primary red-sea theater), never in SSA,
   even though the corridor physically borders Djibouti/Eritrea. Same for
   Panama missing from LatAm.

   `getRegionCorridors` now unions direct theater membership (via
   CORRIDORS.theaterId) with indirect claims (via THEATERS.corridorIds),
   de-duplicated by corridor id. This reflects geopolitical reality:
     - MENA + SSA both see babelm threat events
     - NA + LatAm both see panama threat events
   Scoring impact: SSA maritime_access now weighs babelm (weight 0.9),
   LatAm maritime_access now weighs panama (weight 0.6). These were
   missing buffers under the pre-fix model.

   Added regression tests for both new paths. The existing
   "SSA evidence has no chokepoints" test was inverted to assert SSA now
   DOES include babelm (and excludes MENA/East Asia corridors).
koala73 added a commit that referenced this pull request May 18, 2026
…ated inputs as stale (#3728)

Two independent defects in the regional snapshot meta builder were both
fabricating snapshot_confidence and valid_until:

1. classifyInputs() in freshness.mjs defaulted present-but-undated
   payloads to "fresh" — a stalled seeder that left an old payload
   without a timestamp would silently inflate snapshot_confidence.
   Flipped the default to "stale": you cannot prove freshness without
   a timestamp.

2. buildPreMeta() in snapshot-meta.mjs hardcoded
   valid_until = now + 6h, ignoring the per-input maxAgeMin in
   FRESHNESS_REGISTRY. Replaced with
   min(ts + maxAgeMin*60_000) across fresh inputs, clamped to
   [now, now + 6h]. When no inputs are fresh, valid_until collapses
   to now.

Updates tests/regional-snapshot.test.mjs (5 new cases) and the
pre-existing mobility test that asserted the old undated-as-fresh
behavior. Resolves TODO #181 (pending since PR #2940).

Closes #3728
koala73 added a commit that referenced this pull request May 18, 2026
…d_until (#3728) (#3781)

* fix(regional-snapshot): derive valid_until from input TTLs; treat undated inputs as stale (#3728)

Two independent defects in the regional snapshot meta builder were both
fabricating snapshot_confidence and valid_until:

1. classifyInputs() in freshness.mjs defaulted present-but-undated
   payloads to "fresh" — a stalled seeder that left an old payload
   without a timestamp would silently inflate snapshot_confidence.
   Flipped the default to "stale": you cannot prove freshness without
   a timestamp.

2. buildPreMeta() in snapshot-meta.mjs hardcoded
   valid_until = now + 6h, ignoring the per-input maxAgeMin in
   FRESHNESS_REGISTRY. Replaced with
   min(ts + maxAgeMin*60_000) across fresh inputs, clamped to
   [now, now + 6h]. When no inputs are fresh, valid_until collapses
   to now.

Updates tests/regional-snapshot.test.mjs (5 new cases) and the
pre-existing mobility test that asserted the old undated-as-fresh
behavior. Resolves TODO #181 (pending since PR #2940).

Closes #3728

* fix(regional-snapshot): wire 5 upstream feeds to seed-meta to prevent on-deploy false STALE (#3781 follow-on)

Static analysis identified 5 Redis keys whose payloads carry no
timestamp `extractTimestamp` recognises and would therefore classify
as STALE on first deploy under #3728's new logic. All five already
have a `seed-meta:*` companion written by existing seeders (and
tracked by `api/health.js`); we just point the freshness registry at
them.

- risk:scores:sebuf:stale:v1          → seed-meta:intelligence:risk-scores
- intelligence:cross-source-signals:v1 → seed-meta:intelligence:cross-source-signals
- energy:mix:v1:_all                   → seed-meta:economic:owid-energy-mix
- supply_chain:transit-summaries:v1    → seed-meta:supply_chain:transit-summaries
- relay:oref:history:v1                → seed-meta:relay:oref:history (new write
  added next to existing envelopeWrite in ais-relay.cjs)

Tests lock in the wiring so a later removal trips a unit test, not production.

* fix(regional-snapshot): address review #3781 — gate seed-meta on envelope ok, tick-consistent valid_until, fix wrong-assertion test, parameterize metaKey resolution

Review round-2 fixes for PR #3781:

- ais-relay.cjs: gate the seed-meta:relay:oref:history write on envelope
  write success. Previously the meta write could land alone if the
  envelope write failed (Upstash 5xx / blip), letting the freshness
  classifier report FRESH for data that does not exist in Redis. Added
  a note about the pre-existing asymmetry at the transit-summaries
  write (~line 7370), which is intentionally out of scope here.
- snapshot-meta.mjs: snapshot `Date.now()` once in buildPreMeta and
  pass it as a 4th arg to deriveValidUntil so the two-step compute is
  tick-consistent. Removes the need for timing tolerance in tests that
  assert exact valid_until values.
- regional-snapshot.test.mjs: split the misnamed 6h-cap test into
  (a) "tightest input wins" and (b) "cap binds when all inputs out-TTL
  the cap". The previous test claimed to exercise the cap but the
  15-min input always won. New (b) restricts the sources map to
  registry entries with maxAgeMin > 6h so the cap is actually binding.
- regional-snapshot.test.mjs: parameterize the metaKey-resolution test
  across all 5 newly-wired keys (risk:scores, cross-source-signals,
  energy:mix, transit-summaries, relay:oref:history) so removing any
  single metaKey trips a test.
@koala73
koala73 deleted the worktree-compiled-weaving-riddle branch June 25, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant