mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
feat(resilience): SWF manifest expansion + KIA split + new schema fields (#3391)
* feat(resilience): SWF manifest expansion + KIA split + new schema fields Phase 1 of plan 2026-04-25-001 (Codex-approved round 5). Manifest-only data correction; no construct change, no cache prefix bump. Schema additions (loader-validated, misplacement-rejected): - top-level: aum_usd, aum_year, aum_verified (primary-source AUM) - under classification: aum_pct_of_audited (fraction multiplier), excluded_overlaps_with_reserves (boolean; documentation-only) Manifest expansion (13 → 21 funds, 6 → 13 countries): - UAE: +ICD ($320B verified), +ADQ ($199B verified), +EIA (unverified — loaded for documentation, excluded from scoring per data-integrity rule) - KW: kia split into kia-grf (5%, access=0.9) + kia-fgf (95%, access=0.20). Corrects ~18× over-statement of crisis-deployable Kuwait sovereign wealth (audit found combined-AUM × 0.7 access applied $750B as "deployable" against ~$15B actual GRF stabilization capacity). - CN: +CIC ($1.35T), +NSSF ($400B, statutorily-gated 0.20 tier), +SAFE-IC ($417B, excluded — overlaps SAFE FX reserves) - HK: +HKMA-EF ($498B, excluded — overlaps HKMA reserves) - KR: +KIC ($182B, IFSWF full member) - AU: +Future Fund ($192B, pension-locked) - OM: +OIA ($50B, IFSWF member) - BH: +Mumtalakat ($19B) - TL: +Petroleum Fund ($22B, GPFG-style high-transparency) Re-audits (Phase 1E): - ADIA access 0.3 → 0.4 (rubric flagged; ruler-discretionary deployment empirically demonstrated) - Mubadala access 0.4 → 0.5 (rubric flagged); transparency 0.6 → 0.7 (LM=10 + IFSWF full member alignment) Rubric (docs/methodology/swf-classification-rubric.md): - New "Statutorily-gated long-horizon" 0.20 access tier added between 0.1 (sanctions/frozen) and 0.3 (intergenerational/ruler-discretionary). Anchored by KIA-FGF (Decree 106 of 1976; Council-of-Ministers + Emir decree gate; crossed once in extremis during COVID). Seeder: - Two new pure helpers: shouldSkipFundForBuffer (excluded/unverified decision) and applyAumPctOfAudited (sleeve fraction multiplier) - Manifest-AUM bypass: if aum_verified=true AND aum_usd present, use that value directly (skip Wikipedia) - Skip funds with excluded_overlaps_with_reserves=true (no double-counting against reserveAdequacy / liquidReserveAdequacy) - Skip funds with aum_verified=false (load for documentation only) Tests (+25 net): - 15 schema-extension tests (misplacement rejection, value-range gates, rationale-pairing coherence, backward-compat with pre-PR entries) - 10 helper tests (shouldSkipFundForBuffer + applyAumPctOfAudited predicates and arithmetic; KIA-GRF + KIA-FGF sum equals combined AUM) - Existing manifest test updated for the kia → kia-grf+kia-fgf split Full suite: 6,940 tests pass (+50 net), typecheck clean, no new lint. Predicted ranking deltas (informational, NOT acceptance criteria per plan §"Hard non-goals"): - AE sovFiscBuf likely 39 → 47-49 (Phase 1A + 1E) - KW sovFiscBuf likely 98 → 53-57 (Phase 1B) - CN, HK (excluded), KR, AU acquire newly-defined sovFiscBuf scores - GCC ordering shifts toward QA > KW > AE; AE-KW gap likely 6 → ~3-4 Real outcome will be measured post-deploy via cohort audit per plan §Phase 4. * fix(resilience): completeness denominator excludes documentation-only funds PR-3391 review (P1 catch): the per-country `expectedFunds` denominator counted ALL manifest entries (`funds.length`) including those skipped from buffer scoring by design — `excluded_overlaps_with_reserves: true` (SAFE-IC, HKMA-EF) and `aum_verified: false` (EIA). Result: countries with mixed scorable + non-scorable rosters showed `completeness < 1.0` even when every scorable fund matched. UAE (4 scorable + EIA) would show 0.8; CN (CIC + NSSF + SAFE-IC excluded) would show 0.67. The downstream scorer then derated those countries' coverage based on a fake-partial signal. Three call sites all carried the same bug: - per-country `expectedFunds` in fetchSovereignWealth main loop - `expectedFundsTotal` + `expectedCountries` in buildCoverageSummary - `countManifestFundsForCountry` (missing-country path) All three now filter via `shouldSkipFundForBuffer` to count only scorable manifest entries. Documentation-only funds neither expected nor matched — they don't appear in the ratio at all. Tests added (+4): - AE complete with all 4 scorable matched (EIA documented but excluded) - CN complete with CIC + NSSF matched (SAFE-IC documented but excluded) - Missing-country path returns scorable count not raw manifest count - Country with ONLY documentation-only entries excluded from expectedCountries Full suite: 6,944 tests pass (+4 net), typecheck clean. * fix(resilience): address Greptile P2s on PR #3391 manifest Three review findings, all in the manifest YAML: 1. **KIA-GRF access 0.9 → 0.7** (rubric alignment): GRF deployment requires active Council-of-Ministers authorization (2020 COVID precedent demonstrates this), not rule-triggered automatic deployment. The rubric's 0.9 tier ("Pure automatic stabilization") reserved for funds where political authorization is post-hoc / symbolic (Chile ESSF candidate). KIA-GRF correctly fits 0.7 ("Explicit stabilization with rule") — the same tier the pre-split combined-KIA was assigned. Updated rationale clarifies the tier choice. Rubric's 0.7 precedent column already lists "KIA General Reserve Fund" — now consistent with the manifest. 2. **Duplicate `# ── Australia ──` header before Oman** (copy-paste artifact): removed the orphaned header at the Oman section; added proper `# ── Australia ──` header above the Future Fund entry where it actually belongs (after Timor-Leste). 3. **NSSF `aum_pct_of_audited: 1.0` removed** (no-op): a multiplier of 1.0 is identity. The schema field is OPTIONAL and only meant for fund-of-funds split entries (e.g. KIA-GRF/FGF). Setting it to 1.0 forced the loader to require an `aum_pct_of_audited` rationale paragraph with no computational benefit. Both the field and the paragraph are now removed; NSSF remains a single- sleeve entry that scores its full audited AUM. Full suite: 6,944 tests pass, typecheck clean.
This commit is contained in:
@@ -53,6 +53,7 @@ Deployment SPEED (weeks vs months vs years) is the core signal.
|
|||||||
| Tier | Value | Meaning | Concrete precedents |
|
| Tier | Value | Meaning | Concrete precedents |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Nil access | **0.1** | Sanctions, asset freeze, or political paralysis makes deployment effectively impossible within a crisis window | Russia NWF (post-2022 asset freeze), Libya LIA (sanctions + frozen assets), Iran NDFI (sanctions + access concerns). Currently deferred from v1 for this reason. |
|
| Nil access | **0.1** | Sanctions, asset freeze, or political paralysis makes deployment effectively impossible within a crisis window | Russia NWF (post-2022 asset freeze), Libya LIA (sanctions + frozen assets), Iran NDFI (sanctions + access concerns). Currently deferred from v1 for this reason. |
|
||||||
|
| Statutorily-gated long-horizon | **0.20** | Withdrawals require statutory supermajority / bicameral-equivalent action; gate has been crossed in extreme cases (single, capped draw under emergency law) but NOT for ordinary stabilization. Distinct from "Intergenerational savings" (0.3) because the gate is *statutory* rather than ruler-discretionary — Council-of-Ministers + parliamentary or constitutional thresholds replace head-of-state direction. | KIA Future Generations Fund (Decree 106 of 1976; Council-of-Ministers + Emir decree required; gate crossed once during COVID for a small capped draw). Phase 1B addition (Plan 2026-04-25-001). |
|
||||||
| Intergenerational savings | **0.3** | Pure long-horizon wealth-preservation mandate; no explicit stabilization rule; withdrawal requires ruler / head-of-state / parliamentary discretion with no codified trigger | ADIA (Abu Dhabi, intergenerational mandate, ruler-discretionary); Brunei BIA (deferred candidate) |
|
| Intergenerational savings | **0.3** | Pure long-horizon wealth-preservation mandate; no explicit stabilization rule; withdrawal requires ruler / head-of-state / parliamentary discretion with no codified trigger | ADIA (Abu Dhabi, intergenerational mandate, ruler-discretionary); Brunei BIA (deferred candidate) |
|
||||||
| Hybrid / constrained | **0.5** | Mandate mixes strategic + savings + partial stabilization; deployment is mechanically possible but constrained by strategic allocation locked to policy objectives (Vision 2030, industrial policy, geopolitical holdings) | PIF (Saudi Arabia, Vision 2030-locked), QIA (Qatar, long-horizon wealth-management with amiri-decree deployment), Mubadala (UAE, strategic + financial hybrid), Ireland ISIF (strategic-development mandate) |
|
| Hybrid / constrained | **0.5** | Mandate mixes strategic + savings + partial stabilization; deployment is mechanically possible but constrained by strategic allocation locked to policy objectives (Vision 2030, industrial policy, geopolitical holdings) | PIF (Saudi Arabia, Vision 2030-locked), QIA (Qatar, long-horizon wealth-management with amiri-decree deployment), Mubadala (UAE, strategic + financial hybrid), Ireland ISIF (strategic-development mandate) |
|
||||||
| Explicit stabilization with rule | **0.7** | Legislated or rule-based mechanism for fiscal support during specific shock classes, with historical precedent of actual deployment | KIA General Reserve Fund (legislated finance of budget shortfalls from oil-revenue swings). NO GPFG is BORDERLINE — has a fiscal rule capping withdrawal at ~3% expected real return, which is an access MECHANISM but also an access CONSTRAINT (see below). NOTE: GIC is discussed in the alignment table below as a candidate for this tier based on its NIRC framework, but the current manifest rates it 0.6 — so it's a 0.7 *candidate*, not a 0.7 *precedent*. |
|
| Explicit stabilization with rule | **0.7** | Legislated or rule-based mechanism for fiscal support during specific shock classes, with historical precedent of actual deployment | KIA General Reserve Fund (legislated finance of budget shortfalls from oil-revenue swings). NO GPFG is BORDERLINE — has a fiscal rule capping withdrawal at ~3% expected real return, which is an access MECHANISM but also an access CONSTRAINT (see below). NOTE: GIC is discussed in the alignment table below as a candidate for this tier based on its NIRC framework, but the current manifest rates it 0.6 — so it's a 0.7 *candidate*, not a 0.7 *precedent*. |
|
||||||
|
|||||||
@@ -655,6 +655,56 @@ async function fetchWikipediaInfobox(fund, fxRates) {
|
|||||||
|
|
||||||
// ── Aggregation ──
|
// ── Aggregation ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure predicate: should this manifest fund be SKIPPED from the
|
||||||
|
* SWF buffer calculation? Returns the skip reason string or null.
|
||||||
|
*
|
||||||
|
* Two skip conditions (Phase 1 §schema):
|
||||||
|
* - `excluded_overlaps_with_reserves: true` — AUM already counted
|
||||||
|
* in central-bank FX reserves (SAFE-IC, HKMA-EF). Excluding
|
||||||
|
* prevents double-counting against reserveAdequacy /
|
||||||
|
* liquidReserveAdequacy.
|
||||||
|
* - `aum_verified: false` — fund AUM not primary-source-confirmed.
|
||||||
|
* Loaded for documentation; excluded from scoring per the
|
||||||
|
* data-integrity rule (Codex Round 1 #7).
|
||||||
|
*
|
||||||
|
* Pure function — exported for tests.
|
||||||
|
*
|
||||||
|
* @param {{ classification?: { excludedOverlapsWithReserves?: boolean }, aumVerified?: boolean }} fund
|
||||||
|
* @returns {'excluded_overlaps_with_reserves' | 'aum_unverified' | null}
|
||||||
|
*/
|
||||||
|
export function shouldSkipFundForBuffer(fund) {
|
||||||
|
if (fund?.classification?.excludedOverlapsWithReserves === true) {
|
||||||
|
return 'excluded_overlaps_with_reserves';
|
||||||
|
}
|
||||||
|
if (fund?.aumVerified === false) {
|
||||||
|
return 'aum_unverified';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure helper: apply the `aum_pct_of_audited` multiplier to a
|
||||||
|
* resolved AUM value. When the fund's classification has no
|
||||||
|
* `aum_pct_of_audited`, returns the AUM unchanged.
|
||||||
|
*
|
||||||
|
* Used for fund-of-funds split entries (e.g. KIA-GRF is ~5% of the
|
||||||
|
* audited KIA total; KIA-FGF is ~95%).
|
||||||
|
*
|
||||||
|
* Pure function — exported for tests.
|
||||||
|
*
|
||||||
|
* @param {number} resolvedAumUsd
|
||||||
|
* @param {{ classification?: { aumPctOfAudited?: number } }} fund
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function applyAumPctOfAudited(resolvedAumUsd, fund) {
|
||||||
|
const pct = fund?.classification?.aumPctOfAudited;
|
||||||
|
if (typeof pct === 'number' && pct > 0 && pct <= 1) {
|
||||||
|
return resolvedAumUsd * pct;
|
||||||
|
}
|
||||||
|
return resolvedAumUsd;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchFundAum(fund, wikipediaCache, fxRates) {
|
async function fetchFundAum(fund, wikipediaCache, fxRates) {
|
||||||
// Source priority: official → IFSWF → Wikipedia list → Wikipedia
|
// Source priority: official → IFSWF → Wikipedia list → Wikipedia
|
||||||
// per-fund infobox. Short-circuit on first non-null return so the
|
// per-fund infobox. Short-circuit on first non-null return so the
|
||||||
@@ -779,22 +829,41 @@ export async function fetchSovereignWealth() {
|
|||||||
|
|
||||||
const fundRecords = [];
|
const fundRecords = [];
|
||||||
for (const fund of funds) {
|
for (const fund of funds) {
|
||||||
const aum = await fetchFundAum(fund, wikipediaCache, fxRates);
|
const skipReason = shouldSkipFundForBuffer(fund);
|
||||||
|
if (skipReason) {
|
||||||
|
console.log(`[seed-sovereign-wealth] ${fund.country}:${fund.fund} skipped — ${skipReason}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUM resolution: prefer manifest-provided primary-source AUM
|
||||||
|
// when verified; fall back to the existing Wikipedia/IFSWF
|
||||||
|
// resolution chain otherwise (existing entries that pre-date
|
||||||
|
// the schema extension still work unchanged).
|
||||||
|
let aum = null;
|
||||||
|
if (fund.aumVerified === true && typeof fund.aumUsd === 'number') {
|
||||||
|
aum = { aum: fund.aumUsd, aumYear: fund.aumYear ?? null, source: 'manifest_primary' };
|
||||||
|
} else {
|
||||||
|
aum = await fetchFundAum(fund, wikipediaCache, fxRates);
|
||||||
|
}
|
||||||
if (!aum) {
|
if (!aum) {
|
||||||
unmatched.push(`${fund.country}:${fund.fund}`);
|
unmatched.push(`${fund.country}:${fund.fund}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adjustedAum = applyAumPctOfAudited(aum.aum, fund);
|
||||||
|
const aumPct = fund.classification?.aumPctOfAudited;
|
||||||
sourceMix[aum.source] = (sourceMix[aum.source] ?? 0) + 1;
|
sourceMix[aum.source] = (sourceMix[aum.source] ?? 0) + 1;
|
||||||
|
|
||||||
const { access, liquidity, transparency } = fund.classification;
|
const { access, liquidity, transparency } = fund.classification;
|
||||||
const rawMonths = (aum.aum / denominatorImports) * 12;
|
const rawMonths = (adjustedAum / denominatorImports) * 12;
|
||||||
const effectiveMonths = rawMonths * access * liquidity * transparency;
|
const effectiveMonths = rawMonths * access * liquidity * transparency;
|
||||||
|
|
||||||
fundRecords.push({
|
fundRecords.push({
|
||||||
fund: fund.fund,
|
fund: fund.fund,
|
||||||
aum: aum.aum,
|
aum: adjustedAum,
|
||||||
aumYear: aum.aumYear,
|
aumYear: aum.aumYear,
|
||||||
source: aum.source,
|
source: aum.source,
|
||||||
|
...(aumPct != null ? { aumPctOfAudited: aumPct } : {}),
|
||||||
access,
|
access,
|
||||||
liquidity,
|
liquidity,
|
||||||
transparency,
|
transparency,
|
||||||
@@ -805,9 +874,23 @@ export async function fetchSovereignWealth() {
|
|||||||
|
|
||||||
if (fundRecords.length === 0) continue;
|
if (fundRecords.length === 0) continue;
|
||||||
const totalEffectiveMonths = fundRecords.reduce((s, f) => s + f.effectiveMonths, 0);
|
const totalEffectiveMonths = fundRecords.reduce((s, f) => s + f.effectiveMonths, 0);
|
||||||
const expectedFunds = funds.length;
|
// Completeness denominator excludes funds that were INTENTIONALLY
|
||||||
|
// skipped from buffer scoring (excluded_overlaps_with_reserves OR
|
||||||
|
// aum_verified=false). Without this, manifest entries that exist
|
||||||
|
// for documentation only would artificially depress completeness
|
||||||
|
// for countries with mixed scorable + non-scorable funds — e.g.
|
||||||
|
// UAE (4 scorable + EIA unverified) would show completeness=0.8
|
||||||
|
// even when every scorable fund matched, and CN (CIC + NSSF
|
||||||
|
// scorable + SAFE-IC excluded) would show 0.67.
|
||||||
|
//
|
||||||
|
// The right denominator is "scorable funds for this country":
|
||||||
|
// funds where shouldSkipFundForBuffer returns null. Documentation-
|
||||||
|
// only entries are neither matched nor expected; they don't appear
|
||||||
|
// in the ratio at all.
|
||||||
|
const scorableFunds = funds.filter((f) => shouldSkipFundForBuffer(f) === null);
|
||||||
|
const expectedFunds = scorableFunds.length;
|
||||||
const matchedFunds = fundRecords.length;
|
const matchedFunds = fundRecords.length;
|
||||||
const completeness = matchedFunds / expectedFunds;
|
const completeness = expectedFunds > 0 ? matchedFunds / expectedFunds : 0;
|
||||||
// `completeness` signals partial-seed on multi-fund countries (AE,
|
// `completeness` signals partial-seed on multi-fund countries (AE,
|
||||||
// SG). Downstream scorer must derate the country when completeness
|
// SG). Downstream scorer must derate the country when completeness
|
||||||
// < 1.0 — silently emitting partial totalEffectiveMonths would
|
// < 1.0 — silently emitting partial totalEffectiveMonths would
|
||||||
@@ -816,7 +899,7 @@ export async function fetchSovereignWealth() {
|
|||||||
// use the partial number for IMPUTE-level coverage), but only
|
// use the partial number for IMPUTE-level coverage), but only
|
||||||
// completeness=1.0 countries count toward recordCount / health.
|
// completeness=1.0 countries count toward recordCount / health.
|
||||||
if (completeness < 1.0) {
|
if (completeness < 1.0) {
|
||||||
console.warn(`[seed-sovereign-wealth] ${iso2} partial: ${matchedFunds}/${expectedFunds} funds matched — completeness=${completeness.toFixed(2)}`);
|
console.warn(`[seed-sovereign-wealth] ${iso2} partial: ${matchedFunds}/${expectedFunds} scorable funds matched — completeness=${completeness.toFixed(2)}`);
|
||||||
}
|
}
|
||||||
countries[iso2] = {
|
countries[iso2] = {
|
||||||
funds: fundRecords,
|
funds: fundRecords,
|
||||||
@@ -886,8 +969,16 @@ export async function fetchSovereignWealth() {
|
|||||||
* @param {Record<string, { matchedFunds: number, expectedFunds: number, completeness: number }>} countries Seeded country payload
|
* @param {Record<string, { matchedFunds: number, expectedFunds: number, completeness: number }>} countries Seeded country payload
|
||||||
*/
|
*/
|
||||||
export function buildCoverageSummary(manifest, imports, countries) {
|
export function buildCoverageSummary(manifest, imports, countries) {
|
||||||
const expectedFundsTotal = manifest.funds.length;
|
// Coverage denominator excludes manifest entries that are
|
||||||
const expectedCountries = new Set(manifest.funds.map((f) => f.country));
|
// documentation-only by design — funds with
|
||||||
|
// `excluded_overlaps_with_reserves: true` (SAFE-IC, HKMA-EF) or
|
||||||
|
// `aum_verified: false` (EIA). Counting them as "expected" would
|
||||||
|
// depress the headline coverage ratio for countries with mixed
|
||||||
|
// scorable + non-scorable fund rosters. Same fix as the per-country
|
||||||
|
// completeness denominator above; see comment there.
|
||||||
|
const scorableManifestFunds = manifest.funds.filter((f) => shouldSkipFundForBuffer(f) === null);
|
||||||
|
const expectedFundsTotal = scorableManifestFunds.length;
|
||||||
|
const expectedCountries = new Set(scorableManifestFunds.map((f) => f.country));
|
||||||
let matchedFundsTotal = 0;
|
let matchedFundsTotal = 0;
|
||||||
for (const entry of Object.values(countries)) matchedFundsTotal += entry.matchedFunds;
|
for (const entry of Object.values(countries)) matchedFundsTotal += entry.matchedFunds;
|
||||||
// Every status carries a `reason` field so downstream consumers that
|
// Every status carries a `reason` field so downstream consumers that
|
||||||
@@ -925,8 +1016,18 @@ export function buildCoverageSummary(manifest, imports, countries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function countManifestFundsForCountry(manifest, iso2) {
|
function countManifestFundsForCountry(manifest, iso2) {
|
||||||
|
// Counts SCORABLE funds for the given country (excludes documentation-
|
||||||
|
// only entries: `excluded_overlaps_with_reserves: true` and
|
||||||
|
// `aum_verified: false`). Used by buildCoverageSummary's missing-
|
||||||
|
// country path so the "expected" figure on a missing country reflects
|
||||||
|
// what the seeder would actually try to score, not all manifest
|
||||||
|
// entries.
|
||||||
let n = 0;
|
let n = 0;
|
||||||
for (const f of manifest.funds) if (f.country === iso2) n++;
|
for (const f of manifest.funds) {
|
||||||
|
if (f.country !== iso2) continue;
|
||||||
|
if (shouldSkipFundForBuffer(f) !== null) continue;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
# re-runs the seeder against the new entry to confirm 8/N live match.
|
# re-runs the seeder against the new entry to confirm 8/N live match.
|
||||||
|
|
||||||
manifest_version: 1
|
manifest_version: 1
|
||||||
last_reviewed: 2026-04-23
|
last_reviewed: 2026-04-25
|
||||||
# REVIEWED means: coefficients derive from the committed rationale +
|
# REVIEWED means: coefficients derive from the committed rationale +
|
||||||
# sources block and the seeder end-to-end matches the expected funds
|
# sources block and the seeder end-to-end matches the expected funds
|
||||||
# against the live Wikipedia / IFSWF / official-disclosure surfaces.
|
# against the live Wikipedia / IFSWF / official-disclosure surfaces.
|
||||||
@@ -111,13 +111,26 @@ funds:
|
|||||||
abbrev: ADIA
|
abbrev: ADIA
|
||||||
fund_name: Abu Dhabi Investment Authority
|
fund_name: Abu Dhabi Investment Authority
|
||||||
classification:
|
classification:
|
||||||
access: 0.3
|
# Phase 1E re-audit (Plan 2026-04-25-001): bumped from 0.3 → 0.4.
|
||||||
|
# ADIA's official mandate is intergenerational, but its
|
||||||
|
# ruler-discretionary deployment pattern (2009 Mubadala bailout
|
||||||
|
# precedent, recurring budget-support contributions to Abu Dhabi
|
||||||
|
# treasury) reflects higher empirical access than the 0.3
|
||||||
|
# "intergenerational" tier suggests. The 0.4 mid-tier value sits
|
||||||
|
# between intergenerational (0.3) and hybrid-constrained (0.5).
|
||||||
|
access: 0.4
|
||||||
liquidity: 0.7
|
liquidity: 0.7
|
||||||
transparency: 0.5
|
transparency: 0.5
|
||||||
rationale:
|
rationale:
|
||||||
access: |
|
access: |
|
||||||
Intergenerational savings mandate; no explicit stabilization
|
Official mandate is long-horizon intergenerational savings,
|
||||||
access rule. Ruler-discretionary deployment. Low-medium access.
|
but ruler-discretionary deployment has been demonstrated:
|
||||||
|
Mubadala 2009 bailout precedent, periodic budget-support
|
||||||
|
contributions to Abu Dhabi treasury, strategic infusions
|
||||||
|
during Dubai's 2009 GCC crisis. Empirical access falls
|
||||||
|
between the strict intergenerational tier (0.3) and the
|
||||||
|
hybrid-constrained tier (0.5). Phase 1E re-audit bumped
|
||||||
|
the score from 0.3 → 0.4 to reflect this.
|
||||||
liquidity: |
|
liquidity: |
|
||||||
ADIA 2024 review discloses ~55-70% public-market (equities +
|
ADIA 2024 review discloses ~55-70% public-market (equities +
|
||||||
bonds) allocation, balance in alternatives and real assets.
|
bonds) allocation, balance in alternatives and real assets.
|
||||||
@@ -134,24 +147,143 @@ funds:
|
|||||||
wikipedia:
|
wikipedia:
|
||||||
fund_name: Mubadala Investment Company
|
fund_name: Mubadala Investment Company
|
||||||
classification:
|
classification:
|
||||||
access: 0.4
|
# Phase 1E re-audit (Plan 2026-04-25-001): rubric flagged
|
||||||
|
# Mubadala's pre-PR access=0.4 as below the 0.5 hybrid-tier
|
||||||
|
# midpoint and transparency=0.6 as under-rated for an LM=10
|
||||||
|
# IFSWF full member. Both bumped to align with the rubric.
|
||||||
|
access: 0.5
|
||||||
liquidity: 0.5
|
liquidity: 0.5
|
||||||
transparency: 0.6
|
transparency: 0.7
|
||||||
rationale:
|
rationale:
|
||||||
access: |
|
access: |
|
||||||
Strategic + financial hybrid mandate — combines economic-
|
Strategic + financial hybrid mandate — combines economic-
|
||||||
diversification assets with financial investments. Medium
|
diversification assets with financial investments. The 2024
|
||||||
access for fiscal support; constrained by strategic holdings.
|
ADQ-related corporate restructuring (consolidation of Abu
|
||||||
|
Dhabi state investment vehicles) reinforces Mubadala's
|
||||||
|
treatment as a hybrid-constrained 0.5-tier vehicle: deployable
|
||||||
|
for fiscal support, constrained by strategic holdings.
|
||||||
|
Access bumped from 0.4 → 0.5 in Phase 1E re-audit.
|
||||||
liquidity: |
|
liquidity: |
|
||||||
Mixed: ~50% public equities + credit, ~50% private equity,
|
Mixed: ~50% public equities + credit, ~50% private equity,
|
||||||
real estate, infrastructure (Mubadala 2024 annual report).
|
real estate, infrastructure (Mubadala 2024 annual report).
|
||||||
transparency: |
|
transparency: |
|
||||||
Audited AUM published, asset-mix disclosed annually. IFSWF
|
Audited AUM published, asset-mix disclosed annually. IFSWF
|
||||||
member. LM index = 10.
|
full member. LM index = 10. Bumped from 0.6 → 0.7 in
|
||||||
|
Phase 1E re-audit to align with the rubric tier (0.7 = audited
|
||||||
|
AUM + asset-class mix + returns disclosed annually).
|
||||||
sources:
|
sources:
|
||||||
- https://www.mubadala.com/en/annual-review
|
- https://www.mubadala.com/en/annual-review
|
||||||
- https://www.ifswf.org/member-profiles/mubadala-investment-company
|
- https://www.ifswf.org/member-profiles/mubadala-investment-company
|
||||||
|
|
||||||
|
# Investment Corporation of Dubai — Dubai-government holding
|
||||||
|
# company. Owns Emirates Airlines, ENBD, Dubai Holdings, and
|
||||||
|
# significant Emaar / DEWA stakes. Distinct from ADIA (Abu Dhabi
|
||||||
|
# emirate) and Mubadala (Abu Dhabi strategic). Phase 1A addition.
|
||||||
|
- country: AE
|
||||||
|
fund: icd
|
||||||
|
display_name: Investment Corporation of Dubai (ICD)
|
||||||
|
wikipedia:
|
||||||
|
abbrev: ICD
|
||||||
|
fund_name: Investment Corporation of Dubai
|
||||||
|
aum_usd: 320000000000
|
||||||
|
aum_year: 2023
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.5
|
||||||
|
liquidity: 0.5
|
||||||
|
transparency: 0.4
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Dubai-government holding company; finances Dubai-emirate
|
||||||
|
budget directly during shocks. 2009 GCC bailout precedent
|
||||||
|
when ICD-affiliated entities (Dubai World, Nakheel) were
|
||||||
|
actively rolled into broader fiscal support. Hybrid-
|
||||||
|
constrained 0.5 tier — deployment is mechanically possible
|
||||||
|
but constrained by strategic holdings (Emirates Airlines,
|
||||||
|
ENBD).
|
||||||
|
liquidity: |
|
||||||
|
Mixed portfolio: Emaar + ENBD + DEWA + Borse Dubai listed
|
||||||
|
equity stakes (~50% public market) balanced against
|
||||||
|
Emirates Airlines + Dubai Holdings real estate (~50%
|
||||||
|
private). 0.5 mid-liquidity tier.
|
||||||
|
transparency: |
|
||||||
|
ICD publishes consolidated audited financial highlights but
|
||||||
|
does not disclose holdings-level detail. LM index ~4.
|
||||||
|
Annual review available via icd.gov.ae but constituent
|
||||||
|
AUM disclosure is partial.
|
||||||
|
sources:
|
||||||
|
- https://www.icd.gov.ae/en/about-icd/our-portfolio/
|
||||||
|
- https://www.icd.gov.ae/en/news-and-publications/
|
||||||
|
|
||||||
|
# ADQ (Abu Dhabi Developmental Holding Company) — strategic
|
||||||
|
# investment vehicle established 2018, distinct from ADIA and
|
||||||
|
# Mubadala. Phase 1A addition.
|
||||||
|
- country: AE
|
||||||
|
fund: adq
|
||||||
|
display_name: Abu Dhabi Developmental Holding Company (ADQ)
|
||||||
|
wikipedia:
|
||||||
|
abbrev: ADQ
|
||||||
|
fund_name: ADQ
|
||||||
|
aum_usd: 199000000000
|
||||||
|
aum_year: 2024
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.5
|
||||||
|
liquidity: 0.4
|
||||||
|
transparency: 0.4
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Hybrid strategic-investment vehicle under Abu Dhabi
|
||||||
|
government control. Mandate covers economic diversification
|
||||||
|
+ strategic asset stewardship. Medium access for fiscal
|
||||||
|
support; access discipline closer to Mubadala than to ADIA's
|
||||||
|
long-horizon savings tier.
|
||||||
|
liquidity: |
|
||||||
|
Heavy in private equity, food + agriculture (Agthia, Modern
|
||||||
|
Bakery), real estate, healthcare, and energy. Limited public-
|
||||||
|
market exposure relative to ADIA. ~0.4 mid-low liquidity.
|
||||||
|
transparency: |
|
||||||
|
ADQ publishes AUM and asset-class summaries via corporate
|
||||||
|
press releases and select press disclosures (Reuters, FT
|
||||||
|
primary reporting). IFSWF observer-only. LM index ~4.
|
||||||
|
sources:
|
||||||
|
- https://www.adq.ae/about-us/
|
||||||
|
- https://www.adq.ae/news-and-insights/
|
||||||
|
|
||||||
|
# Emirates Investment Authority (EIA) — federal-level UAE wealth
|
||||||
|
# vehicle distinct from emirate-level (ADIA, ICD, ADQ, Mubadala)
|
||||||
|
# funds. Limited public disclosure — primary-source AUM not
|
||||||
|
# verifiable as of 2026-04-25; entry loaded for documentation but
|
||||||
|
# excluded from buffer scoring per data-integrity rule
|
||||||
|
# (`aum_verified: false`). Phase 1A addition; revisit when EIA
|
||||||
|
# publishes audited consolidated statements.
|
||||||
|
- country: AE
|
||||||
|
fund: eia
|
||||||
|
display_name: Emirates Investment Authority (EIA)
|
||||||
|
aum_verified: false
|
||||||
|
classification:
|
||||||
|
access: 0.4
|
||||||
|
liquidity: 0.5
|
||||||
|
transparency: 0.2
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Federal-level UAE reserves vehicle (cf. emirate-level ADIA/
|
||||||
|
ICD). Mandate covers federal fiscal stabilization for
|
||||||
|
emirate-fiscal-shock smoothing. Access is constrained by
|
||||||
|
federal/emirate political coordination; rated mid-low.
|
||||||
|
liquidity: |
|
||||||
|
Limited public disclosure; mix presumed to mirror federal-
|
||||||
|
reserves convention (majority public-market) but not
|
||||||
|
verified.
|
||||||
|
transparency: |
|
||||||
|
Limited public disclosure. Reuters cites federal-level AUM
|
||||||
|
figures but EIA itself does not publish audited annual
|
||||||
|
statements at the level of ADIA / Mubadala / ICD. LM index
|
||||||
|
~2. Marked `aum_verified: false` until primary disclosure
|
||||||
|
materializes.
|
||||||
|
sources:
|
||||||
|
- https://www.eia.gov.ae/en/
|
||||||
|
|
||||||
# ── Saudi Arabia ──
|
# ── Saudi Arabia ──
|
||||||
# PIF combines stabilization, strategic-diversification, and domestic
|
# PIF combines stabilization, strategic-diversification, and domestic
|
||||||
# development mandates. Asset mix is heavily domestic-strategic
|
# development mandates. Asset mix is heavily domestic-strategic
|
||||||
@@ -182,29 +314,97 @@ funds:
|
|||||||
- https://www.ifswf.org/members
|
- https://www.ifswf.org/members
|
||||||
|
|
||||||
# ── Kuwait ──
|
# ── Kuwait ──
|
||||||
# KIA runs two legally distinct funds: General Reserve Fund (budget-
|
# KIA's audited $1.072T AUM is split here per Kuwaiti Public Funds
|
||||||
# financing) and Future Generations Fund (intergenerational). Combined
|
# Law and Decree 106 of 1976 (FGF) into two sleeves with materially
|
||||||
# here since audited AUM is reported at the KIA level.
|
# different access profiles. Phase 1B addition (Plan 2026-04-25-001).
|
||||||
|
# Combined-AUM modeling overstated the crisis-deployable balance by
|
||||||
|
# ~18× (2026-04-24 audit) because GRF's `access=0.7` haircut was
|
||||||
|
# applied to the full $1.072T, when ~95% of that AUM is FGF and
|
||||||
|
# constitutionally gated. The split correctly attributes GRF's
|
||||||
|
# stabilization mandate to its own ~5% sleeve and FGF's
|
||||||
|
# statutorily-gated long-horizon profile to the remaining ~95%.
|
||||||
- country: KW
|
- country: KW
|
||||||
fund: kia
|
fund: kia-grf
|
||||||
display_name: Kuwait Investment Authority (KIA)
|
display_name: Kuwait Investment Authority — General Reserve Fund (KIA-GRF)
|
||||||
wikipedia:
|
wikipedia:
|
||||||
|
# Wikipedia/SWFI report the COMBINED audited KIA AUM; the loader
|
||||||
|
# multiplies by aum_pct_of_audited (5% for GRF, 95% for FGF) to
|
||||||
|
# recover per-sleeve effective balance. No Wikipedia abbrev
|
||||||
|
# specifically for GRF.
|
||||||
abbrev: KIA
|
abbrev: KIA
|
||||||
fund_name: Kuwait Investment Authority
|
fund_name: Kuwait Investment Authority
|
||||||
classification:
|
classification:
|
||||||
access: 0.7
|
access: 0.7
|
||||||
liquidity: 0.8
|
liquidity: 0.8
|
||||||
transparency: 0.4
|
transparency: 0.4
|
||||||
|
aum_pct_of_audited: 0.05
|
||||||
rationale:
|
rationale:
|
||||||
access: |
|
access: |
|
||||||
General Reserve Fund explicitly finances budget shortfalls from
|
Kuwaiti Public Finance Law explicitly directs GRF to absorb
|
||||||
oil-revenue swings. Strongest stabilization access in the Gulf.
|
oil-revenue shortfalls and finance budget deficits. Drained
|
||||||
|
to negative balance during 2020 COVID shock; refilled by
|
||||||
|
domestic + international borrowing. The 2020 deployment
|
||||||
|
required active Council-of-Ministers authorization (NOT
|
||||||
|
post-hoc/symbolic), which keeps GRF in the rubric's 0.7
|
||||||
|
"Explicit stabilization with rule" tier — a legislated
|
||||||
|
mechanism with deployment precedent — rather than the 0.9
|
||||||
|
"automatic stabilization" tier (which requires
|
||||||
|
rule-triggered automatic deployment, e.g. Chile ESSF).
|
||||||
|
The original combined-KIA `access=0.7` matched this tier;
|
||||||
|
kept here for the GRF sleeve.
|
||||||
liquidity: |
|
liquidity: |
|
||||||
Predominantly public-market (~75-85% listed equities + fixed
|
Predominantly public-market (~75-85% listed equities + fixed
|
||||||
income). Private-asset sleeve is a minority allocation.
|
income). Private-asset sleeve is a minority allocation.
|
||||||
|
Same portfolio profile as KIA-FGF — classification independent.
|
||||||
transparency: |
|
transparency: |
|
||||||
Financials reported to National Assembly but sealed from
|
Financials reported to National Assembly but sealed from
|
||||||
public; partial IFSWF engagement. LM index = 6.
|
public; partial IFSWF engagement. LM index = 6.
|
||||||
|
aum_pct_of_audited: |
|
||||||
|
GRF receives oil revenues and finances the budget; its
|
||||||
|
steady-state balance is roughly 5% of KIA's combined audited
|
||||||
|
AUM. The fraction varies year-to-year (negative in 2020-21
|
||||||
|
during COVID, refilled by 2022-23). Using a representative
|
||||||
|
steady-state share avoids dependency on year-specific
|
||||||
|
balances that aren't separately disclosed.
|
||||||
|
sources:
|
||||||
|
- https://www.kia.gov.kw/en/
|
||||||
|
- https://www.ifswf.org/member-profiles/kuwait-investment-authority
|
||||||
|
|
||||||
|
- country: KW
|
||||||
|
fund: kia-fgf
|
||||||
|
display_name: Kuwait Investment Authority — Future Generations Fund (KIA-FGF)
|
||||||
|
wikipedia:
|
||||||
|
abbrev: KIA
|
||||||
|
fund_name: Kuwait Investment Authority
|
||||||
|
classification:
|
||||||
|
access: 0.20
|
||||||
|
liquidity: 0.8
|
||||||
|
transparency: 0.4
|
||||||
|
aum_pct_of_audited: 0.95
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
FGF withdrawals require Council-of-Ministers + Emir decree
|
||||||
|
(Decree 106 of 1976; 2020 amendment did NOT remove the gate,
|
||||||
|
only added an emergency-pathway provision used once during
|
||||||
|
COVID for a small, capped draw). Not legally accessible for
|
||||||
|
ordinary stabilization. Score 0.20 reflects: (a) below the
|
||||||
|
0.3 "intergenerational/ruler-discretionary" tier because
|
||||||
|
the gate is bicameral-equivalent + statutory, (b) above
|
||||||
|
the 0.1 "sanctions/frozen" tier because the gate has been
|
||||||
|
crossed in extremis. Anchors a NEW rubric tier between 0.1
|
||||||
|
and 0.3 (see swf-classification-rubric.md "Statutorily-gated
|
||||||
|
long-horizon" tier added in Phase 1B).
|
||||||
|
liquidity: |
|
||||||
|
Same portfolio profile as GRF; classification independent.
|
||||||
|
transparency: |
|
||||||
|
Same portfolio profile as GRF; reported to National Assembly
|
||||||
|
but sealed from public; partial IFSWF engagement. LM index = 6.
|
||||||
|
aum_pct_of_audited: |
|
||||||
|
FGF receives 10% of state revenue annually + accumulated
|
||||||
|
returns; in steady state holds ~95% of KIA's combined
|
||||||
|
audited AUM. The 5/95 split is per Kuwait's Public Funds
|
||||||
|
Law and is the canonical proportion published by KIA in
|
||||||
|
IFSWF disclosures.
|
||||||
sources:
|
sources:
|
||||||
- https://www.kia.gov.kw/en/
|
- https://www.kia.gov.kw/en/
|
||||||
- https://www.ifswf.org/member-profiles/kuwait-investment-authority
|
- https://www.ifswf.org/member-profiles/kuwait-investment-authority
|
||||||
@@ -302,6 +502,304 @@ funds:
|
|||||||
- https://www.temasekreview.com.sg/
|
- https://www.temasekreview.com.sg/
|
||||||
- https://www.temasek.com.sg/en/our-financials
|
- https://www.temasek.com.sg/en/our-financials
|
||||||
|
|
||||||
|
# ── China ──
|
||||||
|
# Phase 1C addition (Plan 2026-04-25-001). Three CN sovereign-wealth
|
||||||
|
# vehicles tracked by SWFI/IFSWF; SAFE Investment Co is excluded
|
||||||
|
# from the buffer dim because its AUM is part of the SAFE umbrella
|
||||||
|
# consolidated FX reserves (already counted in reserveAdequacy /
|
||||||
|
# liquidReserveAdequacy).
|
||||||
|
- country: CN
|
||||||
|
fund: cic
|
||||||
|
display_name: China Investment Corporation (CIC)
|
||||||
|
wikipedia:
|
||||||
|
abbrev: CIC
|
||||||
|
fund_name: China Investment Corporation
|
||||||
|
aum_usd: 1350000000000
|
||||||
|
aum_year: 2023
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.4
|
||||||
|
liquidity: 0.5
|
||||||
|
transparency: 0.3
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Hybrid strategic-investment vehicle with PBOC + MOF
|
||||||
|
coordination required for major redeployment. Long-horizon
|
||||||
|
external mandate (CIC International) plus state-directed
|
||||||
|
domestic financial holdings (Central Huijin) — flexible in
|
||||||
|
principle but politically constrained. 0.4 mid-tier.
|
||||||
|
liquidity: |
|
||||||
|
~50% public-market (listed equities + bonds via CIC
|
||||||
|
International), ~50% private (Central Huijin domestic banking
|
||||||
|
stakes, alternative investments). Mid-liquidity.
|
||||||
|
transparency: |
|
||||||
|
Annual report publishes AUM and asset-class summary; holdings-
|
||||||
|
level disclosure limited to large public stakes (13F-equivalent
|
||||||
|
for U.S. holdings). LM index ~3-4.
|
||||||
|
sources:
|
||||||
|
- http://www.china-inv.cn/en/
|
||||||
|
- https://www.swfinstitute.org/profile/cic
|
||||||
|
|
||||||
|
- country: CN
|
||||||
|
fund: nssf
|
||||||
|
display_name: National Council for Social Security Fund (NSSF)
|
||||||
|
wikipedia:
|
||||||
|
fund_name: National Council for Social Security Fund
|
||||||
|
aum_usd: 400000000000
|
||||||
|
aum_year: 2023
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.20
|
||||||
|
liquidity: 0.5
|
||||||
|
transparency: 0.4
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Pension-purpose: NSSF holdings are statutorily reserved for
|
||||||
|
social-security payment obligations. Withdrawals for
|
||||||
|
non-pension fiscal stabilization are not permitted under
|
||||||
|
current Chinese law. Maps to the 0.20 "statutorily-gated
|
||||||
|
long-horizon" tier added in Phase 1B (KIA-FGF analogue).
|
||||||
|
liquidity: |
|
||||||
|
Mix of listed equities, fixed income, and strategic
|
||||||
|
unlisted holdings (banking IPO seedings). Mid-liquidity.
|
||||||
|
transparency: |
|
||||||
|
Annual report publishes AUM totals + broad allocation; per-
|
||||||
|
holding disclosure limited.
|
||||||
|
sources:
|
||||||
|
- http://www.ssf.gov.cn/
|
||||||
|
- https://www.swfinstitute.org/profile/nssf
|
||||||
|
|
||||||
|
- country: CN
|
||||||
|
fund: safe-ic
|
||||||
|
display_name: SAFE Investment Company Limited
|
||||||
|
aum_usd: 417000000000
|
||||||
|
aum_year: 2024
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.5
|
||||||
|
liquidity: 0.7
|
||||||
|
transparency: 0.3
|
||||||
|
excluded_overlaps_with_reserves: true
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Subsidiary of the State Administration of Foreign Exchange
|
||||||
|
(SAFE); manages a portion of China's FX reserves abroad.
|
||||||
|
Documentation-only — see excluded rationale.
|
||||||
|
liquidity: |
|
||||||
|
Predominantly listed equities + sovereign bonds (FX-reserve-
|
||||||
|
like portfolio composition).
|
||||||
|
transparency: |
|
||||||
|
Limited public disclosure; AUM tracked via SWFI third-party
|
||||||
|
estimates. LM index ~2.
|
||||||
|
excluded_overlaps_with_reserves: |
|
||||||
|
SAFE Investment Co's AUM is part of China's State Administration
|
||||||
|
of Foreign Exchange consolidated reserves. Including it in the
|
||||||
|
SWF buffer would double-count against `reserveAdequacy` /
|
||||||
|
`liquidReserveAdequacy`, both of which already capture
|
||||||
|
SAFE-managed FX reserves. Documentation-only entry; excluded
|
||||||
|
from buffer scoring.
|
||||||
|
sources:
|
||||||
|
- https://www.swfinstitute.org/profile/safe-investment-company
|
||||||
|
|
||||||
|
# ── Hong Kong ──
|
||||||
|
# HKMA Exchange Fund. Backing Portfolio is reserves-equivalent
|
||||||
|
# (already in reserveAdequacy); Investment Portfolio + Future Fund
|
||||||
|
# branch could be SWF-relevant but the consolidated AUM is
|
||||||
|
# reported as one figure. Excluded from buffer to avoid double-
|
||||||
|
# counting against HK monetary-authority reserves.
|
||||||
|
- country: HK
|
||||||
|
fund: hkma-ef
|
||||||
|
display_name: HKMA Exchange Fund
|
||||||
|
aum_usd: 498000000000
|
||||||
|
aum_year: 2024
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.7
|
||||||
|
liquidity: 0.9
|
||||||
|
transparency: 0.7
|
||||||
|
excluded_overlaps_with_reserves: true
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Statutory mandate: maintain HKD peg + back banking system.
|
||||||
|
High access for monetary stabilization; stabilization is the
|
||||||
|
primary mandate. Documentation-only — see excluded rationale.
|
||||||
|
liquidity: |
|
||||||
|
Predominantly listed equities + sovereign bonds + USD cash.
|
||||||
|
Highly liquid by design (Backing Portfolio is reserves).
|
||||||
|
transparency: |
|
||||||
|
HKMA publishes monthly Exchange Fund balance sheet + annual
|
||||||
|
report. LM index ~8.
|
||||||
|
excluded_overlaps_with_reserves: |
|
||||||
|
HKMA Exchange Fund's Backing Portfolio is reserves-equivalent
|
||||||
|
and is captured under `reserveAdequacy` / `liquidReserveAdequacy`.
|
||||||
|
Investment Portfolio + Future Fund are not separately disclosed
|
||||||
|
at AUM level. To avoid double-counting, excluded from buffer.
|
||||||
|
Implementation-Time Unknown #2 in the parent plan flagged this
|
||||||
|
for follow-up: when HKMA discloses Investment Portfolio AUM
|
||||||
|
separately, the Investment Portfolio sleeve could be added as
|
||||||
|
a non-excluded entry.
|
||||||
|
sources:
|
||||||
|
- https://www.hkma.gov.hk/eng/data-publications-and-research/
|
||||||
|
|
||||||
|
# ── South Korea ──
|
||||||
|
- country: KR
|
||||||
|
fund: kic
|
||||||
|
display_name: Korea Investment Corporation (KIC)
|
||||||
|
wikipedia:
|
||||||
|
abbrev: KIC
|
||||||
|
fund_name: Korea Investment Corporation
|
||||||
|
aum_usd: 182000000000
|
||||||
|
aum_year: 2024
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.3
|
||||||
|
liquidity: 0.7
|
||||||
|
transparency: 0.7
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Long-horizon mandate; KIC manages assets under MOEF + Bank
|
||||||
|
of Korea entrustment but does not have an explicit
|
||||||
|
stabilization mandate. Withdrawals require entrustment
|
||||||
|
agreement modification. Intergenerational tier.
|
||||||
|
liquidity: |
|
||||||
|
~70% public-market (listed equities + sovereign + corporate
|
||||||
|
bonds), ~30% alternatives (private equity, real estate,
|
||||||
|
infrastructure, hedge funds).
|
||||||
|
transparency: |
|
||||||
|
IFSWF full member. Annual report published with detailed
|
||||||
|
asset allocation, returns, and partial holdings. LM index ~7.
|
||||||
|
sources:
|
||||||
|
- https://www.kic.kr/en/
|
||||||
|
- https://www.ifswf.org/member-profiles/korea-investment-corporation
|
||||||
|
|
||||||
|
# ── Oman ──
|
||||||
|
# OIA established 2020 by merging State General Reserve Fund (SGRF)
|
||||||
|
# + Oman Investment Fund (OIF). IFSWF member; rubric pre-flagged
|
||||||
|
# as "Shippable". Phase 1D addition.
|
||||||
|
- country: OM
|
||||||
|
fund: oia
|
||||||
|
display_name: Oman Investment Authority (OIA)
|
||||||
|
wikipedia:
|
||||||
|
abbrev: OIA
|
||||||
|
fund_name: Oman Investment Authority
|
||||||
|
aum_usd: 50000000000
|
||||||
|
aum_year: 2024
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.5
|
||||||
|
liquidity: 0.5
|
||||||
|
transparency: 0.5
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Hybrid mandate: SGRF (now part of OIA) historically funded
|
||||||
|
budget shortfalls during oil downturns; OIF (now part of OIA)
|
||||||
|
was the strategic vehicle. Combined OIA inherits both;
|
||||||
|
deployment for fiscal support is mechanically possible but
|
||||||
|
coordinated through Ministry of Finance. Mid-tier hybrid.
|
||||||
|
liquidity: |
|
||||||
|
Mixed: external public-market (managed by external managers)
|
||||||
|
+ domestic strategic stakes + alternative investments. Mid.
|
||||||
|
transparency: |
|
||||||
|
OIA publishes annual review post-2020 merger; IFSWF full
|
||||||
|
member. LM index ~6.
|
||||||
|
sources:
|
||||||
|
- https://www.oia.gov.om/
|
||||||
|
- https://www.ifswf.org/member-profiles/oman-investment-authority
|
||||||
|
|
||||||
|
# ── Bahrain ──
|
||||||
|
- country: BH
|
||||||
|
fund: mumtalakat
|
||||||
|
display_name: Mumtalakat Holding Company
|
||||||
|
wikipedia:
|
||||||
|
fund_name: Mumtalakat Holding Company
|
||||||
|
aum_usd: 19000000000
|
||||||
|
aum_year: 2024
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.4
|
||||||
|
liquidity: 0.4
|
||||||
|
transparency: 0.6
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Bahraini state strategic-investment vehicle. Holdings
|
||||||
|
concentrated in domestic banking (BBK, NBB), aluminum
|
||||||
|
(ALBA), telecoms (Batelco). Deployable for fiscal support
|
||||||
|
via dividend flow but not via primary-asset liquidation
|
||||||
|
without disrupting strategic positions.
|
||||||
|
liquidity: |
|
||||||
|
Domestic strategic holdings + foreign-listed equity stakes.
|
||||||
|
Mid-low liquidity.
|
||||||
|
transparency: |
|
||||||
|
Audited annual report, asset-class disclosures. IFSWF
|
||||||
|
member. LM index ~7.
|
||||||
|
sources:
|
||||||
|
- https://www.mumtalakat.bh/en
|
||||||
|
- https://www.ifswf.org/member-profiles/mumtalakat-holding-company
|
||||||
|
|
||||||
|
# ── Timor-Leste ──
|
||||||
|
# Petroleum Fund of Timor-Leste — rubric flagged as
|
||||||
|
# "high-transparency textbook fit". High-transparency fund
|
||||||
|
# benchmark; Banco Central de Timor-Leste publishes monthly
|
||||||
|
# statements + annual reports. Phase 1D addition.
|
||||||
|
- country: TL
|
||||||
|
fund: petroleum-fund
|
||||||
|
display_name: Petroleum Fund of Timor-Leste
|
||||||
|
aum_usd: 22000000000
|
||||||
|
aum_year: 2024
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.7
|
||||||
|
liquidity: 0.9
|
||||||
|
transparency: 0.9
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Norwegian-model petroleum fund: fiscal-rule-based annual
|
||||||
|
Estimated Sustainable Income (ESI) drawdown for budget
|
||||||
|
support. Statutorily codified deployment trigger; closer
|
||||||
|
to GPFG's rule-based stabilization than to KIA-FGF's
|
||||||
|
long-horizon lock. 0.7 explicit-stabilization tier.
|
||||||
|
liquidity: |
|
||||||
|
~60% sovereign bonds, ~40% global equities. Highly liquid.
|
||||||
|
transparency: |
|
||||||
|
Monthly statements published by Banco Central de Timor-Leste;
|
||||||
|
annual report with full holdings disclosure. LM index ~9.
|
||||||
|
sources:
|
||||||
|
- https://www.bancocentral.tl/en/petroleum-fund
|
||||||
|
- https://www.swfinstitute.org/profile/petroleum-fund-of-timor-leste
|
||||||
|
|
||||||
|
# ── Australia ──
|
||||||
|
- country: AU
|
||||||
|
fund: future-fund
|
||||||
|
display_name: Australian Future Fund
|
||||||
|
wikipedia:
|
||||||
|
fund_name: Future Fund (Australia)
|
||||||
|
aum_usd: 192000000000
|
||||||
|
aum_year: 2024
|
||||||
|
aum_verified: true
|
||||||
|
classification:
|
||||||
|
access: 0.3
|
||||||
|
liquidity: 0.5
|
||||||
|
transparency: 0.8
|
||||||
|
rationale:
|
||||||
|
access: |
|
||||||
|
Established 2006 to fund Commonwealth of Australia
|
||||||
|
unfunded superannuation liabilities. Statutorily restricted
|
||||||
|
from drawdown until 2027 (originally — extended). Long-
|
||||||
|
horizon savings tier. Australian fiscal practice has used
|
||||||
|
Future Fund AUM as a buffer signal in budget discussions
|
||||||
|
but no operational drawdown has occurred for stabilization.
|
||||||
|
liquidity: |
|
||||||
|
Mixed: ~30% listed equities, ~25% alternatives, ~20% bonds,
|
||||||
|
~15% real estate + infrastructure, ~10% private equity.
|
||||||
|
Mid-liquidity.
|
||||||
|
transparency: |
|
||||||
|
Quarterly portfolio updates with asset-class breakdowns;
|
||||||
|
annual report with detailed performance + holdings discussion.
|
||||||
|
LM index ~9.
|
||||||
|
sources:
|
||||||
|
- https://www.futurefund.gov.au/
|
||||||
|
- https://www.swfinstitute.org/profile/australia-future-fund
|
||||||
|
|
||||||
# ────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────
|
||||||
# CANDIDATES DEFERRED FROM V1
|
# CANDIDATES DEFERRED FROM V1
|
||||||
# ────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ const MANIFEST_PATH = resolve(here, './swf-classification-manifest.yaml');
|
|||||||
* @property {number} access 0..1 inclusive
|
* @property {number} access 0..1 inclusive
|
||||||
* @property {number} liquidity 0..1 inclusive
|
* @property {number} liquidity 0..1 inclusive
|
||||||
* @property {number} transparency 0..1 inclusive
|
* @property {number} transparency 0..1 inclusive
|
||||||
|
* @property {number} [aumPctOfAudited] OPTIONAL 0..1; multiplier applied
|
||||||
|
* to the matched audited AUM, used
|
||||||
|
* when one entry represents only a
|
||||||
|
* fraction of a combined audited
|
||||||
|
* fund (e.g. KIA-GRF vs KIA-FGF
|
||||||
|
* split of audited KIA AUM).
|
||||||
|
* @property {boolean} [excludedOverlapsWithReserves] OPTIONAL; when true,
|
||||||
|
* the seeder loads the entry for
|
||||||
|
* documentation but EXCLUDES it
|
||||||
|
* from buffer calculation. Used
|
||||||
|
* for funds whose AUM is already
|
||||||
|
* counted in central-bank FX
|
||||||
|
* reserves (SAFE Investment Co,
|
||||||
|
* HKMA Exchange Fund) to avoid
|
||||||
|
* double-counting against the
|
||||||
|
* reserveAdequacy /
|
||||||
|
* liquidReserveAdequacy dims.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,8 +69,20 @@ const MANIFEST_PATH = resolve(here, './swf-classification-manifest.yaml');
|
|||||||
* @property {string} displayName human-readable fund name
|
* @property {string} displayName human-readable fund name
|
||||||
* @property {SwfWikipediaHints} [wikipedia] optional lookup hints for the
|
* @property {SwfWikipediaHints} [wikipedia] optional lookup hints for the
|
||||||
* Wikipedia fallback scraper
|
* Wikipedia fallback scraper
|
||||||
|
* @property {number} [aumUsd] OPTIONAL primary-source AUM in USD.
|
||||||
|
* When present AND `aumVerified === true`,
|
||||||
|
* the seeder uses this value directly
|
||||||
|
* instead of resolving via Wikipedia.
|
||||||
|
* @property {number} [aumYear] OPTIONAL year of the primary-source
|
||||||
|
* AUM disclosure (e.g. 2024).
|
||||||
|
* @property {boolean} [aumVerified] OPTIONAL primary-source-confirmed flag.
|
||||||
|
* When false, the entry is loaded for
|
||||||
|
* documentation but EXCLUDED from buffer
|
||||||
|
* scoring (data-integrity rule).
|
||||||
* @property {SwfClassification} classification
|
* @property {SwfClassification} classification
|
||||||
* @property {{ access: string, liquidity: string, transparency: string }} rationale
|
* @property {{ access: string, liquidity: string, transparency: string,
|
||||||
|
* [aum_pct_of_audited]: string,
|
||||||
|
* [excluded_overlaps_with_reserves]: string }} rationale
|
||||||
* @property {string[]} sources
|
* @property {string[]} sources
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -93,7 +122,35 @@ function validateClassification(cls, path) {
|
|||||||
assertZeroToOne(c.access, `${path}.access`);
|
assertZeroToOne(c.access, `${path}.access`);
|
||||||
assertZeroToOne(c.liquidity, `${path}.liquidity`);
|
assertZeroToOne(c.liquidity, `${path}.liquidity`);
|
||||||
assertZeroToOne(c.transparency, `${path}.transparency`);
|
assertZeroToOne(c.transparency, `${path}.transparency`);
|
||||||
return { access: c.access, liquidity: c.liquidity, transparency: c.transparency };
|
|
||||||
|
// OPTIONAL: aum_pct_of_audited multiplier (KIA-GRF/FGF split case).
|
||||||
|
let aumPctOfAudited;
|
||||||
|
if (c.aum_pct_of_audited != null) {
|
||||||
|
if (typeof c.aum_pct_of_audited !== 'number'
|
||||||
|
|| Number.isNaN(c.aum_pct_of_audited)
|
||||||
|
|| c.aum_pct_of_audited <= 0
|
||||||
|
|| c.aum_pct_of_audited > 1) {
|
||||||
|
fail(`${path}.aum_pct_of_audited: expected number in (0, 1], got ${JSON.stringify(c.aum_pct_of_audited)}`);
|
||||||
|
}
|
||||||
|
aumPctOfAudited = c.aum_pct_of_audited;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIONAL: excluded_overlaps_with_reserves flag (SAFE-IC / HKMA case).
|
||||||
|
let excludedOverlapsWithReserves;
|
||||||
|
if (c.excluded_overlaps_with_reserves != null) {
|
||||||
|
if (typeof c.excluded_overlaps_with_reserves !== 'boolean') {
|
||||||
|
fail(`${path}.excluded_overlaps_with_reserves: expected boolean, got ${JSON.stringify(c.excluded_overlaps_with_reserves)}`);
|
||||||
|
}
|
||||||
|
excludedOverlapsWithReserves = c.excluded_overlaps_with_reserves;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
access: c.access,
|
||||||
|
liquidity: c.liquidity,
|
||||||
|
transparency: c.transparency,
|
||||||
|
...(aumPctOfAudited != null ? { aumPctOfAudited } : {}),
|
||||||
|
...(excludedOverlapsWithReserves != null ? { excludedOverlapsWithReserves } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateRationale(rat, path) {
|
function validateRationale(rat, path) {
|
||||||
@@ -102,7 +159,19 @@ function validateRationale(rat, path) {
|
|||||||
assertNonEmptyString(r.access, `${path}.access`);
|
assertNonEmptyString(r.access, `${path}.access`);
|
||||||
assertNonEmptyString(r.liquidity, `${path}.liquidity`);
|
assertNonEmptyString(r.liquidity, `${path}.liquidity`);
|
||||||
assertNonEmptyString(r.transparency, `${path}.transparency`);
|
assertNonEmptyString(r.transparency, `${path}.transparency`);
|
||||||
return { access: r.access, liquidity: r.liquidity, transparency: r.transparency };
|
// Optional rationale paragraphs for the new schema fields. Required
|
||||||
|
// ONLY when the corresponding classification field is present (paired
|
||||||
|
// with a rationale in validateFundEntry).
|
||||||
|
const out = { access: r.access, liquidity: r.liquidity, transparency: r.transparency };
|
||||||
|
if (r.aum_pct_of_audited != null) {
|
||||||
|
assertNonEmptyString(r.aum_pct_of_audited, `${path}.aum_pct_of_audited`);
|
||||||
|
out.aumPctOfAudited = r.aum_pct_of_audited;
|
||||||
|
}
|
||||||
|
if (r.excluded_overlaps_with_reserves != null) {
|
||||||
|
assertNonEmptyString(r.excluded_overlaps_with_reserves, `${path}.excluded_overlaps_with_reserves`);
|
||||||
|
out.excludedOverlapsWithReserves = r.excluded_overlaps_with_reserves;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateSources(sources, path) {
|
function validateSources(sources, path) {
|
||||||
@@ -154,6 +223,19 @@ function validateFundEntry(raw, idx, seenFundKeys) {
|
|||||||
if (!raw || typeof raw !== 'object') fail(`${path}: expected object`);
|
if (!raw || typeof raw !== 'object') fail(`${path}: expected object`);
|
||||||
const f = /** @type {Record<string, unknown>} */ (raw);
|
const f = /** @type {Record<string, unknown>} */ (raw);
|
||||||
|
|
||||||
|
// Misplacement gate. `aum_pct_of_audited` and
|
||||||
|
// `excluded_overlaps_with_reserves` are CLASSIFICATION fields.
|
||||||
|
// If they appear at the top level of a fund entry, the loader
|
||||||
|
// rejects with a clear error rather than silently accepting the
|
||||||
|
// misplaced field (which would be ignored by the schema and
|
||||||
|
// produce wrong scoring). Codex Round 1 #4.
|
||||||
|
if (f.aum_pct_of_audited !== undefined) {
|
||||||
|
fail(`${path}: aum_pct_of_audited must be placed under classification:, not top-level`);
|
||||||
|
}
|
||||||
|
if (f.excluded_overlaps_with_reserves !== undefined) {
|
||||||
|
fail(`${path}: excluded_overlaps_with_reserves must be placed under classification:, not top-level`);
|
||||||
|
}
|
||||||
|
|
||||||
assertIso2(f.country, `${path}.country`);
|
assertIso2(f.country, `${path}.country`);
|
||||||
assertNonEmptyString(f.fund, `${path}.fund`);
|
assertNonEmptyString(f.fund, `${path}.fund`);
|
||||||
assertNonEmptyString(f.display_name, `${path}.display_name`);
|
assertNonEmptyString(f.display_name, `${path}.display_name`);
|
||||||
@@ -162,16 +244,59 @@ function validateFundEntry(raw, idx, seenFundKeys) {
|
|||||||
if (seenFundKeys.has(dedupeKey)) fail(`${path}: duplicate fund identifier ${dedupeKey}`);
|
if (seenFundKeys.has(dedupeKey)) fail(`${path}: duplicate fund identifier ${dedupeKey}`);
|
||||||
seenFundKeys.add(dedupeKey);
|
seenFundKeys.add(dedupeKey);
|
||||||
|
|
||||||
|
// OPTIONAL primary-source AUM fields. When `aum_verified === true`
|
||||||
|
// AND `aum_usd` present, the seeder uses these directly without
|
||||||
|
// querying Wikipedia. When `aum_verified === false`, the entry
|
||||||
|
// is loaded for documentation but EXCLUDED from buffer scoring
|
||||||
|
// (data-integrity rule from plan §Phase 1A).
|
||||||
|
let aumUsd;
|
||||||
|
if (f.aum_usd != null) {
|
||||||
|
if (typeof f.aum_usd !== 'number' || !Number.isFinite(f.aum_usd) || f.aum_usd <= 0) {
|
||||||
|
fail(`${path}.aum_usd: expected positive finite number, got ${JSON.stringify(f.aum_usd)}`);
|
||||||
|
}
|
||||||
|
aumUsd = f.aum_usd;
|
||||||
|
}
|
||||||
|
let aumYear;
|
||||||
|
if (f.aum_year != null) {
|
||||||
|
if (typeof f.aum_year !== 'number' || !Number.isInteger(f.aum_year) || f.aum_year < 2000 || f.aum_year > 2100) {
|
||||||
|
fail(`${path}.aum_year: expected integer year in [2000, 2100], got ${JSON.stringify(f.aum_year)}`);
|
||||||
|
}
|
||||||
|
aumYear = f.aum_year;
|
||||||
|
}
|
||||||
|
let aumVerified;
|
||||||
|
if (f.aum_verified != null) {
|
||||||
|
if (typeof f.aum_verified !== 'boolean') {
|
||||||
|
fail(`${path}.aum_verified: expected boolean, got ${JSON.stringify(f.aum_verified)}`);
|
||||||
|
}
|
||||||
|
aumVerified = f.aum_verified;
|
||||||
|
}
|
||||||
|
// Coherence: if aum_verified === true, both aum_usd and aum_year MUST be present.
|
||||||
|
// (A "verified" entry without an actual value is meaningless.)
|
||||||
|
if (aumVerified === true && (aumUsd == null || aumYear == null)) {
|
||||||
|
fail(`${path}: aum_verified=true requires both aum_usd and aum_year to be present`);
|
||||||
|
}
|
||||||
|
|
||||||
const classification = validateClassification(f.classification, `${path}.classification`);
|
const classification = validateClassification(f.classification, `${path}.classification`);
|
||||||
const rationale = validateRationale(f.rationale, `${path}.rationale`);
|
const rationale = validateRationale(f.rationale, `${path}.rationale`);
|
||||||
const sources = validateSources(f.sources, `${path}.sources`);
|
const sources = validateSources(f.sources, `${path}.sources`);
|
||||||
const wikipedia = validateWikipediaHints(f.wikipedia, `${path}.wikipedia`);
|
const wikipedia = validateWikipediaHints(f.wikipedia, `${path}.wikipedia`);
|
||||||
|
|
||||||
|
// Coherence: rationale MUST cover any classification field that is set.
|
||||||
|
if (classification.aumPctOfAudited != null && rationale.aumPctOfAudited == null) {
|
||||||
|
fail(`${path}.rationale.aum_pct_of_audited: required when classification.aum_pct_of_audited is set`);
|
||||||
|
}
|
||||||
|
if (classification.excludedOverlapsWithReserves === true && rationale.excludedOverlapsWithReserves == null) {
|
||||||
|
fail(`${path}.rationale.excluded_overlaps_with_reserves: required when classification.excluded_overlaps_with_reserves is true`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
country: f.country,
|
country: f.country,
|
||||||
fund: f.fund,
|
fund: f.fund,
|
||||||
displayName: f.display_name,
|
displayName: f.display_name,
|
||||||
...(wikipedia ? { wikipedia } : {}),
|
...(wikipedia ? { wikipedia } : {}),
|
||||||
|
...(aumUsd != null ? { aumUsd } : {}),
|
||||||
|
...(aumYear != null ? { aumYear } : {}),
|
||||||
|
...(aumVerified != null ? { aumVerified } : {}),
|
||||||
classification,
|
classification,
|
||||||
rationale,
|
rationale,
|
||||||
sources,
|
sources,
|
||||||
|
|||||||
@@ -33,20 +33,48 @@ describe('SWF classification manifest — shipped YAML', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('lists the first-release set of funds from plan §3.4', () => {
|
it('lists the first-release set of funds from plan §3.4 (KIA split per Phase 1B)', () => {
|
||||||
|
// Phase 1B (Plan 2026-04-25-001) split the original `KW:kia` entry
|
||||||
|
// into `KW:kia-grf` and `KW:kia-fgf` to correctly attribute GRF's
|
||||||
|
// 0.9 stabilization access to its ~5% sleeve and FGF's 0.20
|
||||||
|
// statutorily-gated access to the remaining ~95%. Both identifiers
|
||||||
|
// are now required.
|
||||||
const expected = new Set([
|
const expected = new Set([
|
||||||
'NO:gpfg',
|
'NO:gpfg',
|
||||||
'AE:adia',
|
'AE:adia',
|
||||||
'AE:mubadala',
|
'AE:mubadala',
|
||||||
'SA:pif',
|
'SA:pif',
|
||||||
'KW:kia',
|
'KW:kia-grf',
|
||||||
|
'KW:kia-fgf',
|
||||||
'QA:qia',
|
'QA:qia',
|
||||||
'SG:gic',
|
'SG:gic',
|
||||||
'SG:temasek',
|
'SG:temasek',
|
||||||
]);
|
]);
|
||||||
const actual = new Set(manifest.funds.map((f) => `${f.country}:${f.fund}`));
|
const actual = new Set(manifest.funds.map((f) => `${f.country}:${f.fund}`));
|
||||||
for (const required of expected) {
|
for (const required of expected) {
|
||||||
assert.ok(actual.has(required), `plan §3.4 required fund missing from manifest: ${required}`);
|
assert.ok(actual.has(required), `plan §3.4 + Phase 1B required fund missing from manifest: ${required}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Phase 1 (Plan 2026-04-25-001) expansion adds 12 new funds across 7 new + extended countries', () => {
|
||||||
|
// Phase 1 expansion: UAE adds ICD/ADQ/EIA (3); KW splits kia → kia-grf+kia-fgf
|
||||||
|
// (1 net since kia is dropped); CN adds CIC/NSSF/SAFE-IC (3); HK adds HKMA-EF
|
||||||
|
// (1); KR adds KIC (1); AU adds Future Fund (1); OM adds OIA (1); BH adds
|
||||||
|
// Mumtalakat (1); TL adds Petroleum Fund (1). Net new identifiers: 12 over
|
||||||
|
// the original 8 + 1 from KIA split. Manifest total ≥ 20.
|
||||||
|
const required = new Set([
|
||||||
|
'AE:icd', 'AE:adq', 'AE:eia',
|
||||||
|
'CN:cic', 'CN:nssf', 'CN:safe-ic',
|
||||||
|
'HK:hkma-ef',
|
||||||
|
'KR:kic',
|
||||||
|
'AU:future-fund',
|
||||||
|
'OM:oia',
|
||||||
|
'BH:mumtalakat',
|
||||||
|
'TL:petroleum-fund',
|
||||||
|
]);
|
||||||
|
const actual = new Set(manifest.funds.map((f) => `${f.country}:${f.fund}`));
|
||||||
|
for (const r of required) {
|
||||||
|
assert.ok(actual.has(r), `Phase 1 expansion fund missing from manifest: ${r}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
387
tests/swf-manifest-loader-schema-extension.test.mjs
Normal file
387
tests/swf-manifest-loader-schema-extension.test.mjs
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
// Schema-extension tests for swf-manifest-loader.mjs (Phase 1).
|
||||||
|
//
|
||||||
|
// Pins the new schema fields' canonical placement and rejection rules:
|
||||||
|
// - top-level (per-fund): aum_usd, aum_year, aum_verified
|
||||||
|
// - under classification: aum_pct_of_audited, excluded_overlaps_with_reserves
|
||||||
|
//
|
||||||
|
// Codex Round 1 #4 mandated a SINGLE canonical placement for each new
|
||||||
|
// field, with the loader REJECTING misplacement (positive control)
|
||||||
|
// rather than silently accepting it.
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { validateManifest } from '../scripts/shared/swf-manifest-loader.mjs';
|
||||||
|
import {
|
||||||
|
shouldSkipFundForBuffer,
|
||||||
|
applyAumPctOfAudited,
|
||||||
|
buildCoverageSummary,
|
||||||
|
} from '../scripts/seed-sovereign-wealth.mjs';
|
||||||
|
|
||||||
|
function makeFund(overrides = {}) {
|
||||||
|
return {
|
||||||
|
country: 'AE',
|
||||||
|
fund: 'test-fund',
|
||||||
|
display_name: 'Test Fund',
|
||||||
|
classification: { access: 0.5, liquidity: 0.5, transparency: 0.5 },
|
||||||
|
rationale: { access: 'a', liquidity: 'l', transparency: 't' },
|
||||||
|
sources: ['https://example.com/'],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeManifest(funds) {
|
||||||
|
return {
|
||||||
|
manifest_version: 1,
|
||||||
|
last_reviewed: '2026-04-25',
|
||||||
|
external_review_status: 'REVIEWED',
|
||||||
|
funds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('REJECTS aum_pct_of_audited placed at fund top level (must be under classification)', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({ aum_pct_of_audited: 0.05 }),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m), /aum_pct_of_audited must be placed under classification/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS excluded_overlaps_with_reserves placed at fund top level', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({ excluded_overlaps_with_reserves: true }),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m), /excluded_overlaps_with_reserves must be placed under classification/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACCEPTS aum_pct_of_audited under classification when paired with rationale', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
classification: { access: 0.9, liquidity: 0.8, transparency: 0.4, aum_pct_of_audited: 0.05 },
|
||||||
|
rationale: { access: 'a', liquidity: 'l', transparency: 't', aum_pct_of_audited: 'GRF is ~5% of audited KIA AUM' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const out = validateManifest(m);
|
||||||
|
assert.equal(out.funds[0].classification.aumPctOfAudited, 0.05);
|
||||||
|
assert.equal(out.funds[0].rationale.aumPctOfAudited, 'GRF is ~5% of audited KIA AUM');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS aum_pct_of_audited under classification WITHOUT a rationale paragraph', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
classification: { access: 0.9, liquidity: 0.8, transparency: 0.4, aum_pct_of_audited: 0.05 },
|
||||||
|
// rationale.aum_pct_of_audited is missing
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m),
|
||||||
|
/rationale\.aum_pct_of_audited: required when classification\.aum_pct_of_audited is set/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS aum_pct_of_audited outside (0, 1] range', () => {
|
||||||
|
// `null` is intentionally NOT in this list — the loader treats null
|
||||||
|
// as "field absent" (the value is optional), which is correct.
|
||||||
|
for (const bad of [0, -0.1, 1.5, 'x', NaN]) {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
classification: { access: 0.9, liquidity: 0.8, transparency: 0.4, aum_pct_of_audited: bad },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m), /aum_pct_of_audited: expected number in \(0, 1\]/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACCEPTS excluded_overlaps_with_reserves: true with paired rationale', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
classification: { access: 0.5, liquidity: 0.7, transparency: 0.3, excluded_overlaps_with_reserves: true },
|
||||||
|
rationale: { access: 'a', liquidity: 'l', transparency: 't', excluded_overlaps_with_reserves: 'SAFE-IC overlaps PBOC reserves' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const out = validateManifest(m);
|
||||||
|
assert.equal(out.funds[0].classification.excludedOverlapsWithReserves, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS excluded_overlaps_with_reserves: true WITHOUT rationale paragraph', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
classification: { access: 0.5, liquidity: 0.7, transparency: 0.3, excluded_overlaps_with_reserves: true },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m),
|
||||||
|
/rationale\.excluded_overlaps_with_reserves: required when classification\.excluded_overlaps_with_reserves is true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS excluded_overlaps_with_reserves of non-boolean type', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
classification: { access: 0.5, liquidity: 0.7, transparency: 0.3, excluded_overlaps_with_reserves: 'true' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m), /excluded_overlaps_with_reserves: expected boolean/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACCEPTS aum_usd + aum_year + aum_verified=true together', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
aum_usd: 320_000_000_000,
|
||||||
|
aum_year: 2024,
|
||||||
|
aum_verified: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const out = validateManifest(m);
|
||||||
|
assert.equal(out.funds[0].aumUsd, 320_000_000_000);
|
||||||
|
assert.equal(out.funds[0].aumYear, 2024);
|
||||||
|
assert.equal(out.funds[0].aumVerified, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS aum_verified: true without aum_usd', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
aum_verified: true,
|
||||||
|
aum_year: 2024,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m),
|
||||||
|
/aum_verified=true requires both aum_usd and aum_year to be present/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS aum_verified: true without aum_year', () => {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
aum_verified: true,
|
||||||
|
aum_usd: 100_000_000_000,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m),
|
||||||
|
/aum_verified=true requires both aum_usd and aum_year to be present/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ACCEPTS aum_verified: false (entry loaded for documentation only)', () => {
|
||||||
|
// No aum_usd / aum_year required when verified=false — the entry
|
||||||
|
// documents an unverifiable fund that the seeder will skip from
|
||||||
|
// scoring. This is the EIA / data-integrity-rule path.
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
aum_verified: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const out = validateManifest(m);
|
||||||
|
assert.equal(out.funds[0].aumVerified, false);
|
||||||
|
assert.equal(out.funds[0].aumUsd, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS aum_year out of [2000, 2100]', () => {
|
||||||
|
// `null` excluded — treated as field-absent, intentional.
|
||||||
|
for (const bad of [1999, 2101, 0, -1, 'x']) {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
aum_usd: 100_000_000_000,
|
||||||
|
aum_year: bad,
|
||||||
|
aum_verified: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m), /aum_year/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REJECTS aum_usd of non-positive or non-finite type', () => {
|
||||||
|
// `null` excluded — treated as field-absent, intentional.
|
||||||
|
for (const bad of [0, -1, NaN, Infinity, 'big']) {
|
||||||
|
const m = makeManifest([
|
||||||
|
makeFund({
|
||||||
|
aum_usd: bad,
|
||||||
|
aum_year: 2024,
|
||||||
|
aum_verified: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
assert.throws(() => validateManifest(m), /aum_usd/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Backward-compat: existing entries without new fields still validate', () => {
|
||||||
|
// The 8 existing entries on origin/main don't carry aum_usd /
|
||||||
|
// aum_pct / excluded flags. Ensure the schema extension is purely
|
||||||
|
// additive — existing fields produce a clean parse.
|
||||||
|
const m = makeManifest([makeFund()]);
|
||||||
|
const out = validateManifest(m);
|
||||||
|
assert.equal(out.funds[0].aumUsd, undefined);
|
||||||
|
assert.equal(out.funds[0].aumVerified, undefined);
|
||||||
|
assert.equal(out.funds[0].classification.aumPctOfAudited, undefined);
|
||||||
|
assert.equal(out.funds[0].classification.excludedOverlapsWithReserves, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Seeder-side pure helpers ──────────────────────────────────────
|
||||||
|
|
||||||
|
test('shouldSkipFundForBuffer: returns null for a normal fund', () => {
|
||||||
|
const fund = { classification: { access: 0.5 }, aumVerified: true };
|
||||||
|
assert.equal(shouldSkipFundForBuffer(fund), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shouldSkipFundForBuffer: skips when excluded_overlaps_with_reserves=true', () => {
|
||||||
|
const fund = {
|
||||||
|
classification: { access: 0.5, excludedOverlapsWithReserves: true },
|
||||||
|
aumVerified: true,
|
||||||
|
};
|
||||||
|
assert.equal(shouldSkipFundForBuffer(fund), 'excluded_overlaps_with_reserves');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shouldSkipFundForBuffer: skips when aum_verified=false', () => {
|
||||||
|
const fund = {
|
||||||
|
classification: { access: 0.5 },
|
||||||
|
aumVerified: false,
|
||||||
|
};
|
||||||
|
assert.equal(shouldSkipFundForBuffer(fund), 'aum_unverified');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shouldSkipFundForBuffer: excluded takes precedence over unverified (single skip reason)', () => {
|
||||||
|
// If a fund is BOTH excluded (overlaps reserves) AND unverified,
|
||||||
|
// we surface the excluded reason because that's the more
|
||||||
|
// architectural concern (double-counting risk).
|
||||||
|
const fund = {
|
||||||
|
classification: { excludedOverlapsWithReserves: true },
|
||||||
|
aumVerified: false,
|
||||||
|
};
|
||||||
|
assert.equal(shouldSkipFundForBuffer(fund), 'excluded_overlaps_with_reserves');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shouldSkipFundForBuffer: returns null when neither flag is set', () => {
|
||||||
|
// Backward-compat: existing entries on origin/main don't carry
|
||||||
|
// aumVerified or excludedOverlapsWithReserves. They must NOT skip.
|
||||||
|
assert.equal(shouldSkipFundForBuffer({ classification: { access: 0.5 } }), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shouldSkipFundForBuffer: handles malformed / null input defensively', () => {
|
||||||
|
assert.equal(shouldSkipFundForBuffer(null), null);
|
||||||
|
assert.equal(shouldSkipFundForBuffer(undefined), null);
|
||||||
|
assert.equal(shouldSkipFundForBuffer({}), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applyAumPctOfAudited: returns AUM unchanged when no multiplier set', () => {
|
||||||
|
const fund = { classification: { access: 0.5 } };
|
||||||
|
assert.equal(applyAumPctOfAudited(1_000_000_000_000, fund), 1_000_000_000_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applyAumPctOfAudited: applies the fraction (KIA-GRF case)', () => {
|
||||||
|
// KIA combined audited AUM = $1.072T; GRF is ~5%
|
||||||
|
const fund = { classification: { access: 0.9, aumPctOfAudited: 0.05 } };
|
||||||
|
const out = applyAumPctOfAudited(1_072_000_000_000, fund);
|
||||||
|
assert.equal(out, 53_600_000_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applyAumPctOfAudited: KIA-GRF + KIA-FGF sum equals combined AUM', () => {
|
||||||
|
// The split must be conservative — sum of fractional parts equals
|
||||||
|
// the original audited AUM. Pinned because a future edit that
|
||||||
|
// changes 5/95 split to e.g. 5/90 would silently drop $50B.
|
||||||
|
const audited = 1_072_000_000_000;
|
||||||
|
const grf = applyAumPctOfAudited(audited, { classification: { aumPctOfAudited: 0.05 } });
|
||||||
|
const fgf = applyAumPctOfAudited(audited, { classification: { aumPctOfAudited: 0.95 } });
|
||||||
|
assert.equal(grf + fgf, audited);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applyAumPctOfAudited: ignores out-of-range multipliers (defensive)', () => {
|
||||||
|
// The loader rejects out-of-range values at parse time; this is a
|
||||||
|
// belt-and-suspenders runtime check that doesn't multiply by an
|
||||||
|
// invalid fraction even if the loader's gate is somehow bypassed.
|
||||||
|
for (const bad of [0, -0.1, 1.5, NaN, 'big']) {
|
||||||
|
const fund = { classification: { aumPctOfAudited: bad } };
|
||||||
|
assert.equal(applyAumPctOfAudited(1_000, fund), 1_000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── buildCoverageSummary regression: completeness denominator ──────
|
||||||
|
//
|
||||||
|
// User's PR-3391 review caught a P1: completeness used `funds.length`
|
||||||
|
// (manifest count) as the denominator, which depresses the ratio for
|
||||||
|
// countries whose manifest contains documentation-only entries
|
||||||
|
// (excluded_overlaps_with_reserves OR aum_verified=false). The shipped
|
||||||
|
// manifest has this state for UAE (EIA unverified) and CN (SAFE-IC
|
||||||
|
// excluded). These tests pin the corrected denominator: only scorable
|
||||||
|
// funds count toward expected.
|
||||||
|
|
||||||
|
test('buildCoverageSummary: country with all scorable funds matched is "complete" even if manifest also has unverified entries', () => {
|
||||||
|
// UAE-shape: 4 scorable (ADIA, Mubadala, ICD, ADQ) + 1 unverified (EIA).
|
||||||
|
// If all 4 scorable matched, country is COMPLETE, not partial.
|
||||||
|
const manifest = {
|
||||||
|
funds: [
|
||||||
|
{ country: 'AE', fund: 'adia', classification: { access: 0.4 } },
|
||||||
|
{ country: 'AE', fund: 'mubadala',classification: { access: 0.5 } },
|
||||||
|
{ country: 'AE', fund: 'icd', classification: { access: 0.5 } },
|
||||||
|
{ country: 'AE', fund: 'adq', classification: { access: 0.5 } },
|
||||||
|
{ country: 'AE', fund: 'eia', classification: { access: 0.4 }, aumVerified: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const imports = { AE: { importsUsd: 481.9e9 } };
|
||||||
|
const countries = {
|
||||||
|
AE: {
|
||||||
|
// expectedFunds is computed PER-COUNTRY in fetchSovereignWealth using
|
||||||
|
// shouldSkipFundForBuffer, so this test fixture mirrors the seeder's
|
||||||
|
// post-fix output (expectedFunds = 4 scorable, completeness = 1.0).
|
||||||
|
matchedFunds: 4,
|
||||||
|
expectedFunds: 4,
|
||||||
|
completeness: 1.0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const summary = buildCoverageSummary(manifest, imports, countries);
|
||||||
|
// Only 4 scorable funds in AE; 1 unverified entry doesn't count.
|
||||||
|
assert.equal(summary.expectedFunds, 4,
|
||||||
|
`headline expected funds should exclude documentation-only entries; got ${summary.expectedFunds}`);
|
||||||
|
const aeStatus = summary.countryStatuses.find((s) => s.country === 'AE');
|
||||||
|
assert.equal(aeStatus.status, 'complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildCoverageSummary: excludes excluded_overlaps_with_reserves entries from expectedFundsTotal', () => {
|
||||||
|
// CN-shape: CIC + NSSF scorable + SAFE-IC excluded.
|
||||||
|
const manifest = {
|
||||||
|
funds: [
|
||||||
|
{ country: 'CN', fund: 'cic', classification: { access: 0.4 } },
|
||||||
|
{ country: 'CN', fund: 'nssf', classification: { access: 0.20 } },
|
||||||
|
{ country: 'CN', fund: 'safe-ic', classification: { access: 0.5, excludedOverlapsWithReserves: true } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const imports = { CN: { importsUsd: 3.0e12 } };
|
||||||
|
const countries = {
|
||||||
|
CN: { matchedFunds: 2, expectedFunds: 2, completeness: 1.0 },
|
||||||
|
};
|
||||||
|
const summary = buildCoverageSummary(manifest, imports, countries);
|
||||||
|
assert.equal(summary.expectedFunds, 2,
|
||||||
|
`SAFE-IC should NOT count toward expected funds; got ${summary.expectedFunds}`);
|
||||||
|
const cnStatus = summary.countryStatuses.find((s) => s.country === 'CN');
|
||||||
|
assert.equal(cnStatus.status, 'complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildCoverageSummary: missing-country path uses scorable count, not raw manifest count', () => {
|
||||||
|
// Country with mixed scorable + excluded entries that fails to seed
|
||||||
|
// entirely (e.g. WB imports missing). The "expected" figure on the
|
||||||
|
// missing-country status row should reflect SCORABLE funds, not all
|
||||||
|
// manifest entries — otherwise an operator dashboard shows
|
||||||
|
// "0/3 funds" when the truth is "0/2 funds, 1 documentation-only".
|
||||||
|
const manifest = {
|
||||||
|
funds: [
|
||||||
|
{ country: 'CN', fund: 'cic', classification: { access: 0.4 } },
|
||||||
|
{ country: 'CN', fund: 'nssf', classification: { access: 0.20 } },
|
||||||
|
{ country: 'CN', fund: 'safe-ic', classification: { access: 0.5, excludedOverlapsWithReserves: true } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const imports = {}; // CN imports missing → country not seeded
|
||||||
|
const countries = {}; // no country payload at all
|
||||||
|
const summary = buildCoverageSummary(manifest, imports, countries);
|
||||||
|
const cnStatus = summary.countryStatuses.find((s) => s.country === 'CN');
|
||||||
|
assert.equal(cnStatus.status, 'missing');
|
||||||
|
assert.equal(cnStatus.expected, 2,
|
||||||
|
`missing-country expected should be SCORABLE count (2), not all-manifest (3); got ${cnStatus.expected}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildCoverageSummary: country with ONLY documentation-only entries is excluded from expectedCountries', () => {
|
||||||
|
// Edge case: hypothetical country where every manifest entry is
|
||||||
|
// documentation-only (e.g. only EIA-style unverified). Such a
|
||||||
|
// country has 0 scorable funds → should not appear in
|
||||||
|
// expectedCountries because there's nothing scorable to expect.
|
||||||
|
const manifest = {
|
||||||
|
funds: [
|
||||||
|
{ country: 'XX', fund: 'placeholder', classification: { access: 0.4 }, aumVerified: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const summary = buildCoverageSummary(manifest, {}, {});
|
||||||
|
assert.equal(summary.expectedCountries, 0,
|
||||||
|
`XX has zero scorable funds — should not be in expectedCountries`);
|
||||||
|
assert.equal(summary.expectedFunds, 0);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user