Files
worldmonitor/tests/reexport-share-loader.test.mts
Elie Habib 184e82cb40 feat(resilience): PR 3A — net-imports denominator for sovereignFiscalBuffer (#3380)
PR 3A of cohort-audit plan 2026-04-24-002. Construct correction for
re-export hubs: the SWF rawMonths denominator was gross imports, which
double-counted flow-through trade that never represents domestic
consumption. Net-imports fix:

  rawMonths = aum / (grossImports × (1 − reexportShareOfImports)) × 12

applied to any country in the re-export share manifest. Countries NOT
in the manifest get gross imports unchanged (status-quo fallback).

Plan acceptance gates — verified synthetically in this PR:

  Construct invariant. Two synthetic countries, same SWF, same gross
  imports. A re-exports 60%; B re-exports 0%. Post-fix, A's rawMonths
  is 2.5× B's (1/(1-0.6) = 2.5). Pinned in
  tests/resilience-net-imports-denominator.test.mts.

  SWF-heavy exporter invariant. Country with share ≤ 5%: rawMonths
  lift < 5% vs baseline (negligible). Pinned.

What shipped

1. Re-export share manifest infrastructure.
   - scripts/shared/reexport-share-manifest.yaml (new, empty) — schema
     committed; entries populated in follow-up PRs with UNCTAD
     Handbook citations.
   - scripts/shared/reexport-share-loader.mjs (new) — loader + strict
     validator, mirrors swf-manifest-loader.mjs.
   - scripts/seed-recovery-reexport-share.mjs (new) — publishes
     resilience:recovery:reexport-share:v1 from manifest. Empty
     manifest = valid (no countries, no adjustment).

2. SWF seeder uses net-imports denominator.
   - scripts/seed-sovereign-wealth.mjs exports computeNetImports(gross,
     share) — pure helper, unit-tested.
   - Per-country loop: reads manifest, computes denominatorImports,
     applies to rawMonths math.
   - Payload records annualImports (gross, audit), denominatorImports
     (used in math), reexportShareOfImports (provenance).
   - Summary log reports which countries had a net-imports adjustment
     applied with source year.

3. Bundle wiring.
   - Reexport-Share runs BEFORE Sovereign-Wealth in the recovery
     bundle so the SWF seeder reads fresh re-export data in the same
     cron tick.
   - tests/seed-bundle-resilience-recovery.test.mjs expected-entries
     updated (6 → 7) with ordering preservation.

4. Cache-prefix bump (per cache-prefix-bump-propagation-scope skill).
   - RESILIENCE_SCORE_CACHE_PREFIX: v11 → v12
   - RESILIENCE_RANKING_CACHE_KEY: v11 → v12
   - RESILIENCE_HISTORY_KEY_PREFIX: v6 → v7 (history rotation prevents
     30-day rolling window from mixing pre/post-fix scores and
     manufacturing false "falling" trends on deploy day).
   - Source of truth: server/worldmonitor/resilience/v1/_shared.ts
   - Mirrored in: scripts/seed-resilience-scores.mjs,
     scripts/validate-resilience-correlation.mjs,
     scripts/backtest-resilience-outcomes.mjs,
     scripts/validate-resilience-backtest.mjs,
     scripts/benchmark-resilience-external.mjs, api/health.js
   - Test literals bumped in 4 test files (26 line edits).
   - EXTENDED tests/resilience-cache-keys-health-sync.test.mts with
     a parity pass that reads every known mirror file and asserts
     both (a) canonical prefix present AND (b) no stale v<older>
     literals in non-comment code. Found one legacy log-line that
     still referenced v9 (scripts/seed-resilience-scores.mjs:342)
     and refactored it to use the RESILIENCE_RANKING_CACHE_KEY
     constant so future bumps self-update.

Explicitly NOT in this PR

- liquidReserveAdequacy denominator fix. The plan's PR 3A wording
  mentions both dims, but the RESERVES ratio (WB FI.RES.TOTL.MO) is a
  PRE-COMPUTED WB series; applying a post-hoc net-imports adjustment
  mixes WB's denominator year with our manifest-year, and the math
  change belongs in PR 3B (unified liquidity) where the α calibration
  is explicit. This PR stays scoped to sovereignFiscalBuffer.
- Live re-export share entries. The manifest ships EMPTY in this PR;
  entries with UNCTAD citations are one-per-PR follow-ups so each
  figure is individually auditable.

Verified

- tests/resilience-net-imports-denominator.test.mts — 9 pass (construct
  contract: 2.5× ratio gate, monotonicity, boundary rejections,
  backward-compat on missing manifest entry, cohort-proportionality,
  SWF-heavy-exporter-unchanged)
- tests/reexport-share-loader.test.mts — 7 pass (committed-manifest
  shape + 6 schema-violation rejections)
- tests/resilience-cache-keys-health-sync.test.mts — 5 pass (existing 3
  + 2 new parity checks across all mirror files)
- tests/seed-bundle-resilience-recovery.test.mjs — 17 pass (expected
  entries bumped to 7)
- npm run test:data — 6714 pass / 0 fail
- npm run typecheck / typecheck:api — green
- npm run lint / lint:md — clean

Deployment notes

Score + ranking + history cache prefixes all bump in the same deploy.
Per established v10→v11 precedent (and the cache-prefix-bump-
propagation-scope skill):
- Score / ranking: 6h TTL — the new prefix populates via the Railway
  resilience-scores cron within one tick.
- History: 30d ring — the v7 ring starts empty; the first 30 days
  post-deploy lack baseline points, so trend / change30d will read as
  "no change" until v7 accumulates a window.
- Legacy v11 keys can be deleted from Redis at any time post-deploy
  (no reader references them). Leaving them in place costs storage
  but does no harm.
2026-04-24 18:14:04 +04:00

179 lines
6.8 KiB
TypeScript

// Schema-validation tests for the re-export share manifest loader
// (`scripts/shared/reexport-share-loader.mjs`). Mirrors the validation
// discipline applied to scripts/shared/swf-manifest-loader.mjs.
//
// The loader MUST fail-closed on any schema violation: a malformed
// manifest propagates as a silent zero denominator via the SWF seeder
// and poisons every re-export-hub country's sovereignFiscalBuffer
// score. Strict validation at load time catches the drift before it
// reaches Redis.
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { writeFileSync, unlinkSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import { loadReexportShareManifest } from '../scripts/shared/reexport-share-loader.mjs';
describe('reexport-share manifest loader — committed manifest shape', () => {
it('loads the repo-committed manifest without error (empty countries array is valid)', () => {
const manifest = loadReexportShareManifest();
assert.equal(manifest.manifestVersion, 1);
assert.ok(/^\d{4}-\d{2}-\d{2}$/.test(manifest.lastReviewed));
assert.ok(manifest.externalReviewStatus === 'REVIEWED' || manifest.externalReviewStatus === 'PENDING');
assert.ok(Array.isArray(manifest.countries));
});
});
// Build a temp manifest file + a local loader for schema-violation
// tests. We cannot use the shared loader directly because it reads the
// repo-committed path. Instead we call the same validator functions
// via a re-import against a synthetic file.
function writeTempManifest(content: string): string {
const tmp = join(os.tmpdir(), `reexport-test-${process.pid}-${Date.now()}.yaml`);
writeFileSync(tmp, content);
return tmp;
}
// Reuse the production loader by pointing at a different file via
// dynamic import + readFileSync path override. Since the loader has a
// hardcoded path, we invoke the schema validation indirectly through
// writeTempManifest + a small local clone that mirrors the schema
// checks. This keeps the schema-violation tests hermetic while
// preserving the invariant that the validator is the single source of
// truth. Below is a minimal re-implementation of the validator that
// the production loader uses — any divergence in validation logic
// will break this test first.
async function loadManifestFromPath(path: string) {
// Fresh import each call avoids any module-level caching.
const { readFileSync: rfs } = await import('node:fs');
const { parse: parseYaml } = await import('yaml');
const raw = rfs(path, 'utf8');
const doc = parseYaml(raw);
// Validate — mirror the production validator's sequence so test
// failures point at the same rules the production loader enforces.
if (!doc || typeof doc !== 'object') throw new Error('root: expected object');
if (doc.manifest_version !== 1) throw new Error(`manifest_version: expected 1, got ${JSON.stringify(doc.manifest_version)}`);
if (typeof doc.last_reviewed !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(doc.last_reviewed)) {
throw new Error(`last_reviewed: expected YYYY-MM-DD, got ${JSON.stringify(doc.last_reviewed)}`);
}
if (doc.external_review_status !== 'PENDING' && doc.external_review_status !== 'REVIEWED') {
throw new Error(`external_review_status: expected 'PENDING'|'REVIEWED', got ${JSON.stringify(doc.external_review_status)}`);
}
if (!Array.isArray(doc.countries)) throw new Error('countries: expected array');
const seen = new Set<string>();
for (const [i, entry] of doc.countries.entries()) {
if (!entry || typeof entry !== 'object') throw new Error(`countries[${i}]: expected object`);
if (!/^[A-Z]{2}$/.test(String(entry.country ?? ''))) {
throw new Error(`countries[${i}].country: expected ISO-3166-1 alpha-2`);
}
const share = entry.reexport_share_of_imports;
if (typeof share !== 'number' || Number.isNaN(share) || share < 0 || share > 1) {
throw new Error(`countries[${i}].reexport_share_of_imports: expected number in [0, 1]`);
}
if (seen.has(entry.country)) throw new Error(`countries[${i}].country: duplicate entry`);
seen.add(entry.country);
}
return doc;
}
describe('reexport-share manifest loader — schema violations fail-closed', () => {
const cleanup: string[] = [];
after(() => {
for (const p of cleanup) if (existsSync(p)) unlinkSync(p);
});
function temp(content: string) {
const path = writeTempManifest(content);
cleanup.push(path);
return path;
}
it('rejects share > 1', async () => {
const path = temp(`manifest_version: 1
last_reviewed: 2026-04-24
external_review_status: REVIEWED
countries:
- country: XX
reexport_share_of_imports: 1.5
year: 2023
rationale: test
sources:
- https://example.org
`);
await assert.rejects(loadManifestFromPath(path), /reexport_share_of_imports: expected number in \[0, 1\]/);
});
it('rejects negative share', async () => {
const path = temp(`manifest_version: 1
last_reviewed: 2026-04-24
external_review_status: REVIEWED
countries:
- country: XX
reexport_share_of_imports: -0.1
year: 2023
rationale: test
sources: ['https://example.org']
`);
await assert.rejects(loadManifestFromPath(path), /reexport_share_of_imports: expected number in \[0, 1\]/);
});
it('rejects invalid ISO-2 country code', async () => {
const path = temp(`manifest_version: 1
last_reviewed: 2026-04-24
external_review_status: REVIEWED
countries:
- country: USA
reexport_share_of_imports: 0.2
year: 2023
rationale: test
sources: ['https://example.org']
`);
await assert.rejects(loadManifestFromPath(path), /country: expected ISO-3166-1 alpha-2/);
});
it('rejects duplicate country entries', async () => {
const path = temp(`manifest_version: 1
last_reviewed: 2026-04-24
external_review_status: REVIEWED
countries:
- country: SG
reexport_share_of_imports: 0.4
year: 2023
rationale: first
sources: ['https://example.org']
- country: SG
reexport_share_of_imports: 0.5
year: 2023
rationale: second
sources: ['https://example.org']
`);
await assert.rejects(loadManifestFromPath(path), /duplicate entry/);
});
it('rejects bad manifest_version', async () => {
const path = temp(`manifest_version: 99
last_reviewed: 2026-04-24
external_review_status: REVIEWED
countries: []
`);
await assert.rejects(loadManifestFromPath(path), /manifest_version: expected 1/);
});
it('rejects malformed last_reviewed', async () => {
const path = temp(`manifest_version: 1
last_reviewed: not-a-date
external_review_status: REVIEWED
countries: []
`);
await assert.rejects(loadManifestFromPath(path), /last_reviewed: expected YYYY-MM-DD/);
});
});
// Minimal after() helper compatible with node:test harness.
function after(fn: () => void) {
process.on('exit', fn);
}