feat(resilience): plan 002 §U8 — methodology rewrite + widget polish + net-imports parity (v17.1)#3482
Conversation
…, widget polish + net-imports parity for liquidReserveAdequacy (v17.1) Closes the plan 2026-04-26-002 sequence. Five things land together: 1. **§U8.1 — net-imports denominator parity for liquidReserveAdequacy.** PR #3380 shipped `computeNetImports(grossImports, reexportShareOfImports)` for `sovereignFiscalBuffer` via the SWF seeder, sourced from `resilience:recovery:reexport-share:v1` (Comtrade-backed via PR #3385). The same correction was structurally needed on the sibling `liquidReserveAdequacy` dimension — re-export hubs (AE 35.5%, PA similar) consume WB `FI.RES.TOTL.MO` which is computed at WB source against gross imports, double-counting goods that flow through without settling as domestic consumption. v17.1 multiplies WB's pre-computed months by `1/(1−reexportShare)` for hub countries (algebraic inverse of dividing the denominator) — yields the same adjusted-months a custom `reserves/(net-imports/12)` calc would, without re-fetching raw `FI.RES.TOTL.CD` + `BM.GSR.GNFS.CD` series. Non-hub countries unchanged (status quo). Expected impact at next ranking refresh: AE liquidReserveAdequacy 38 → ~64 (a +26-point dim swing); PA similar magnitude. 2. **§U8 — methodology doc comprehensive v17 changelog.** Single coherent narrative covering the universe + coverage rebuild: source-comprehensiveness flag, coverage penalty multiplier, per-capita normalization with 0.5M tiny-state floor, headline- eligible gate, symmetric cache-hit gate filtering. Anchored to the live `resilience:ranking:v17` cohort numbers captured 2026-04-28 post-#3477 merge: median(Nordics)=78.52 vs median(GCC)=70.53 (+7.98pt gap), min(G7)=64.31 vs max(LIC)=53.73 (+10.58pt gap), 1 microstate (MO at #4) in top-20. Documents 3 known open construct gaps honestly: economic-complexity / industrial-base indicator, importConcentration coverage gap on AE, cyberDigital transient zeros. 3. **§U8 — new doc-parity test (`tests/resilience-doc-parity.test.mts`).** Asserts the doc's prose claims match `_shared.ts` (cache prefix constants), `RESILIENCE_DOMAIN_ORDER` (6 domains), and `RESILIENCE_DIMENSION_ORDER` − `RESILIENCE_RETIRED_DIMENSIONS` (20 active dimensions). Caught real drift: Redis keys table named v11 prefixes despite live v17, and the headline "19 dimensions" claim was off-by-one after PR 2 §3.4 added liquidReserveAdequacy + sovereignFiscalBuffer. Fixed both as part of this commit. 4. **§U8 — widget polish.** `formatResilienceConfidence` now renders "Outside headline ranking" when `headlineEligible: false`, distinct from "Low confidence — sparse data" when `lowConfidence: true`. The two reasons are different and analysts should see them as different. Order matters: lowConfidence is more specific so it wins when both flags fire. New test pins the precedence. 5. **MDX-unsafe `<digit/letter` sweep + `lint:md`.** Both clean. 89 dimension-scorer tests + 36 widget tests + 5 doc-parity tests + 41 ranking + headline-gate tests all green (171 total, 0 fail).
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR closes plan 2026-04-26-002 §U8 with three coordinated changes: a re-export-share denominator correction for Confidence Score: 4/5Safe to merge; all findings are P2 style/performance suggestions with no correctness impact. No P0 or P1 issues. Three P2 findings: the per-country Redis fetch pattern in
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[scoreLiquidReserveAdequacy\ncountryCode, reader] --> B[Fetch reserve-adequacy key]
B --> C{entry found &\nreserveMonths != null?}
C -- No --> D[Return IMPUTE defaults\ncoverage=0, imputedWeight=1]
C -- Yes --> E[readReexportShareForCountry\ncountryCode, reader]
E --> F[Fetch reexport-share:v1 map]
F --> G{share valid?\ntypeof number &&\nfinite && in 0..1}
G -- No --> H[share = null]
G -- Yes --> I[share = reexportShareOfImports]
H --> J[adjustedMonths = reserveMonths]
I --> K[adjustedMonths = reserveMonths / 1-share]
J --> L[normalizeHigherBetter\nmin adjustedMonths 12, 1, 12]
K --> L
L --> M[weightedBlend → ResilienceDimensionScore]
|
| async function readReexportShareForCountry( | ||
| countryCode: string, | ||
| reader: ResilienceSeedReader, | ||
| ): Promise<number | null> { | ||
| const raw = await reader(RESILIENCE_RECOVERY_REEXPORT_SHARE_KEY); | ||
| const payload = raw as { countries?: Record<string, { reexportShareOfImports?: number | null } | undefined> } | null | undefined; | ||
| const share = payload?.countries?.[countryCode]?.reexportShareOfImports; | ||
| if (typeof share !== 'number' || !Number.isFinite(share)) return null; | ||
| if (share < 0 || share >= 1) return null; | ||
| return share; | ||
| } |
There was a problem hiding this comment.
Extra Redis read per country on every scoring cycle
readReexportShareForCountry fetches RESILIENCE_RECOVERY_REEXPORT_SHARE_KEY once per call to scoreLiquidReserveAdequacy, meaning the full reexport-share map is pulled from Redis for every one of the ~220 countries scored, even though only 2 entries (AE + PA) will ever yield a non-null result. If defaultSeedReader doesn't deduplicate/cache within a single scoring run, that's ~220 redundant network round-trips per 6-hour cycle.
A lightweight fix would be to accept the already-fetched map as an optional parameter (mirroring how scoreSovereignFiscalBuffer receives pre-fetched SWF data), or to let the caller fetch the map once and pass it down. Alternatively, if the reader already memoises by key within a run, this is a no-op cost — worth confirming.
…logy-lint compat)
CI methodology-lint test (T1.8) requires every H4 in
docs/methodology/country-resilience-index.mdx to map to a known
scorer dimension via HEADING_TO_DIMENSION — that's how the doc-vs-
registry parity gate works. The v17.1 changelog sub-section I added
(`#### v17.1 — Net-imports denominator parity for liquidReserveAdequacy
(U8.1)`) is a non-dimension subsection and tripped the linter.
Demoted to a bold lead-in paragraph (`**v17.1 — …**`). Same content,
same prose, just no H4. The lint comment explicitly anticipates this
case ("if a future edit adds a legitimate non-dimension H4, either
upgrade it to H3 or add an explicit allowlist") — bold-paragraph is
the cleanest of the three options and matches how Mintlify renders
sub-changelog notes elsewhere in the doc.
Local re-run: methodology-lint + doc-parity 10/10 pass; full test:data
suite 7606/7606 pass, 0 fail.
… doc TTL/dim drift, P2 stale-count tightening)
Three review findings + a Greptile P2 perf note. All addressed locally.
P1 — cache invalidation missing for the §U8.1 scorer-formula change.
liquidReserveAdequacy now adjusts reserveMonths by re-export share for
AE/PA, but the prior commit kept score/ranking at v17 and history at
v12. The `_formula` tag is binary 'd6'|'pc' and does NOT detect intra-
'd6' scorer changes — without this bump, cached v17 AE/PA scores
(gross-imports-denominated) would continue to serve until TTL expiry,
defeating the construct fix this PR delivers. Bumped:
- RESILIENCE_SCORE_CACHE_PREFIX v17 → v18 (_shared.ts:164)
- RESILIENCE_RANKING_CACHE_KEY v17 → v18 (_shared.ts:235)
- RESILIENCE_HISTORY_KEY_PREFIX v12 → v13 (_shared.ts:217)
- script mirrors: seed-resilience-scores.mjs,
backtest, validate, benchmark
- api/health.js: resilienceRanking key
History bumps in lockstep so the rolling 30-day window doesn't mix
pre/post-fix points and manufacture a false "improving" trend on day
one. Same pattern as PR 3A's v11→v12 lockstep for the SWF-side fix.
P2 — Redis keys table TTL + write-gate prose drift. Doc claimed:
resilience:ranking:v17 | 6 hours | "only when all countries are scored"
Actual code: 12h TTL (RESILIENCE_RANKING_CACHE_TTL_SECONDS) and
publishes when coverage >= RANKING_CACHE_MIN_COVERAGE = 0.75. Updated
the table to reflect both. Same correction caught by the parity test
that flagged the v11/v17 drift in the prior commit.
P2 — stale "19 dimensions" claim still in the doc. The v2.0 scorecard
Data row at line 750 still said "19 dimensions across 6 domains"
because the parity test only required ONE correct mention. Tightened
the test to fail on any "<N> dimensions" mention in the
plausible-current-total band [15, 25] that doesn't equal activeCount
or totalCount — historical sub-counts (e.g. v1.0's "13 dimensions",
recovery pillar's "6 dimensions") are outside the band and stay
untouched. Updated the stale line to "20 active dimensions across 6
domains (plus 2 structurally-retired dimensions)".
Bonus — addressed a load of test-side cache-key literal duplications
(plan 002 §U8 follow-up):
- tests/resilience-ranking.test.mts: 27 hardcoded
'resilience:score:v17:XX' / 'resilience:history:v12:XX' literals
swapped to ${RESILIENCE_SCORE_CACHE_PREFIX}/${RESILIENCE_HISTORY_KEY_PREFIX}
template-literal references.
- tests/resilience-handlers.test.mts: same.
- tests/resilience-pillar-aggregation.test.mts: pinned-to-version
assertion replaced with structural-shape regex.
- tests/resilience-scores-seed.test.mjs: same.
This was caught by PR #3477 review for the ranking key; the score and
history prefixes had the same drift trap that resurfaced now. The
proper fix per the project skill (proto-required-field-add-needs-cache-
backfill Trap 5b) is to import the constants — never hardcode literals.
Greptile P2 (per-country Redis read for reexport-share map) — verified
benign: createMemoizedSeedReader caches by key, and _shared.ts:992's
`sharedReader` is shared across all 196 countries in the warm path
(getOrComputeResilienceScores), so reexport-share is fetched ONCE per
ranking compute, not 196 times. Single-country handler creates a fresh
memoized reader per request → 1 read per request. No fix needed.
Verification: 214/214 resilience tests pass, 7606/7606 npm test:data
pass, typecheck clean, lint:md clean.
…ence (#3484) * fix(health): resilienceIntervals maxStaleMin matches real 6h cron cadence The prior comment claimed `intervalMs=2h with hourly Railway fires + 96min skip-window → effective ~2h cadence` and set `maxStaleMin: 360` (= 1× claimed cadence). The deployed Railway cron is actually `0 */6 * * *` (every 6h on the hour, UTC), confirmed by the user 2026-04-28 and corroborated by Railway logs: only 2 successful runs in the seed-bundle-resilience service log over 6h2min, both clean, both writing 196 interval keys. With true cadence=6h and maxStaleMin=360min, ANY normal cron jitter or a slightly-late tick tips resilienceIntervals into STALE_SEED. Reproduced 2026-04-28: seedAgeMin=367 vs maxStale=360 → false alarm despite the bundle running cleanly on schedule. Bumped maxStaleMin 360 → 720 (12h staleness = 2 missed cron ticks). Matches the resilienceRanking entry directly above, written by the SAME cron (refreshRankingAggregate runs in the same Resilience-Scores section). Same pattern as the project's `health-maxstalemin-write- cadence` skill: maxStaleMin must be sized to actual write cadence + grace, not the documented one. Updated the comment to: - Replace the incorrect "intervalMs=2h with hourly fires" claim with the empirically-verified `0 */6 * * *` schedule. - Document the prior values and what each was tuned for, so the next operator who hits this can see why 360 was wrong without re-doing the log-archaeology. - Note the 2026-04-28 incident timestamp so the audit trail is clear. Verification: prefix bump v17→v18 in PR #3482 is in a DIFFERENT namespace (resilience:score:, resilience:ranking:, resilience:history:); this fix is independent and addresses resilience:intervals:v2:* which was unaffected by that PR. * test(health): align cache-keys health-sync test with real 6h cron cadence Review-fix for PR #3484. The existing health-sync test asserted three things that all assumed the wrong (~2h) writer cadence and rejected the new 720 contract: 1. line 216 — exact value 360 → 720 2. line 220 — floor 180 (1.5× 2h) → 540 (1.5× 6h) 3. line 229 — ceiling 480 (4× 2h) → 1080 (3× 6h) Updated all three plus the describe-block label and the rationale comment block at the top. The block now: - Documents both the 2026-04-27 (false-OK at 14d) and 2026-04-28 (false-positive at 360) incidents with root causes for each. - Replaces the in-bundle `intervalMs=2h` cadence claim with the empirically-verified Railway cron `0 */6 * * *` schedule. - Records the full audit trail of prior values (20160 / 1080 / 360 / 720) and what each was tuned for. The `Resilience-Scores section gate is 2h` test stays — that assertion checks the IN-BUNDLE intervalMs in scripts/seed-bundle-resilience.mjs, which is still 2h in the source. Updated only its rationale comment to clarify that the section's in-bundle 2h gate is independent from the OUTER Railway-cron cadence (which is 6h). 13/13 tests in this file pass. Typecheck clean.
…ence (koala73#3484) * fix(health): resilienceIntervals maxStaleMin matches real 6h cron cadence The prior comment claimed `intervalMs=2h with hourly Railway fires + 96min skip-window → effective ~2h cadence` and set `maxStaleMin: 360` (= 1× claimed cadence). The deployed Railway cron is actually `0 */6 * * *` (every 6h on the hour, UTC), confirmed by the user 2026-04-28 and corroborated by Railway logs: only 2 successful runs in the seed-bundle-resilience service log over 6h2min, both clean, both writing 196 interval keys. With true cadence=6h and maxStaleMin=360min, ANY normal cron jitter or a slightly-late tick tips resilienceIntervals into STALE_SEED. Reproduced 2026-04-28: seedAgeMin=367 vs maxStale=360 → false alarm despite the bundle running cleanly on schedule. Bumped maxStaleMin 360 → 720 (12h staleness = 2 missed cron ticks). Matches the resilienceRanking entry directly above, written by the SAME cron (refreshRankingAggregate runs in the same Resilience-Scores section). Same pattern as the project's `health-maxstalemin-write- cadence` skill: maxStaleMin must be sized to actual write cadence + grace, not the documented one. Updated the comment to: - Replace the incorrect "intervalMs=2h with hourly fires" claim with the empirically-verified `0 */6 * * *` schedule. - Document the prior values and what each was tuned for, so the next operator who hits this can see why 360 was wrong without re-doing the log-archaeology. - Note the 2026-04-28 incident timestamp so the audit trail is clear. Verification: prefix bump v17→v18 in PR koala73#3482 is in a DIFFERENT namespace (resilience:score:, resilience:ranking:, resilience:history:); this fix is independent and addresses resilience:intervals:v2:* which was unaffected by that PR. * test(health): align cache-keys health-sync test with real 6h cron cadence Review-fix for PR koala73#3484. The existing health-sync test asserted three things that all assumed the wrong (~2h) writer cadence and rejected the new 720 contract: 1. line 216 — exact value 360 → 720 2. line 220 — floor 180 (1.5× 2h) → 540 (1.5× 6h) 3. line 229 — ceiling 480 (4× 2h) → 1080 (3× 6h) Updated all three plus the describe-block label and the rationale comment block at the top. The block now: - Documents both the 2026-04-27 (false-OK at 14d) and 2026-04-28 (false-positive at 360) incidents with root causes for each. - Replaces the in-bundle `intervalMs=2h` cadence claim with the empirically-verified Railway cron `0 */6 * * *` schedule. - Records the full audit trail of prior values (20160 / 1080 / 360 / 720) and what each was tuned for. The `Resilience-Scores section gate is 2h` test stays — that assertion checks the IN-BUNDLE intervalMs in scripts/seed-bundle-resilience.mjs, which is still 2h in the source. Updated only its rationale comment to clarify that the section's in-bundle 2h gate is independent from the OUTER Railway-cron cadence (which is 6h). 13/13 tests in this file pass. Typecheck clean.
…+ net-imports parity (v17.1) (koala73#3482) * feat(resilience): plan 002 §U8 — methodology rewrite, doc-parity test, widget polish + net-imports parity for liquidReserveAdequacy (v17.1) Closes the plan 2026-04-26-002 sequence. Five things land together: 1. **§U8.1 — net-imports denominator parity for liquidReserveAdequacy.** PR koala73#3380 shipped `computeNetImports(grossImports, reexportShareOfImports)` for `sovereignFiscalBuffer` via the SWF seeder, sourced from `resilience:recovery:reexport-share:v1` (Comtrade-backed via PR koala73#3385). The same correction was structurally needed on the sibling `liquidReserveAdequacy` dimension — re-export hubs (AE 35.5%, PA similar) consume WB `FI.RES.TOTL.MO` which is computed at WB source against gross imports, double-counting goods that flow through without settling as domestic consumption. v17.1 multiplies WB's pre-computed months by `1/(1−reexportShare)` for hub countries (algebraic inverse of dividing the denominator) — yields the same adjusted-months a custom `reserves/(net-imports/12)` calc would, without re-fetching raw `FI.RES.TOTL.CD` + `BM.GSR.GNFS.CD` series. Non-hub countries unchanged (status quo). Expected impact at next ranking refresh: AE liquidReserveAdequacy 38 → ~64 (a +26-point dim swing); PA similar magnitude. 2. **§U8 — methodology doc comprehensive v17 changelog.** Single coherent narrative covering the universe + coverage rebuild: source-comprehensiveness flag, coverage penalty multiplier, per-capita normalization with 0.5M tiny-state floor, headline- eligible gate, symmetric cache-hit gate filtering. Anchored to the live `resilience:ranking:v17` cohort numbers captured 2026-04-28 post-koala73#3477 merge: median(Nordics)=78.52 vs median(GCC)=70.53 (+7.98pt gap), min(G7)=64.31 vs max(LIC)=53.73 (+10.58pt gap), 1 microstate (MO at #4) in top-20. Documents 3 known open construct gaps honestly: economic-complexity / industrial-base indicator, importConcentration coverage gap on AE, cyberDigital transient zeros. 3. **§U8 — new doc-parity test (`tests/resilience-doc-parity.test.mts`).** Asserts the doc's prose claims match `_shared.ts` (cache prefix constants), `RESILIENCE_DOMAIN_ORDER` (6 domains), and `RESILIENCE_DIMENSION_ORDER` − `RESILIENCE_RETIRED_DIMENSIONS` (20 active dimensions). Caught real drift: Redis keys table named v11 prefixes despite live v17, and the headline "19 dimensions" claim was off-by-one after PR 2 §3.4 added liquidReserveAdequacy + sovereignFiscalBuffer. Fixed both as part of this commit. 4. **§U8 — widget polish.** `formatResilienceConfidence` now renders "Outside headline ranking" when `headlineEligible: false`, distinct from "Low confidence — sparse data" when `lowConfidence: true`. The two reasons are different and analysts should see them as different. Order matters: lowConfidence is more specific so it wins when both flags fire. New test pins the precedence. 5. **MDX-unsafe `<digit/letter` sweep + `lint:md`.** Both clean. 89 dimension-scorer tests + 36 widget tests + 5 doc-parity tests + 41 ranking + headline-gate tests all green (171 total, 0 fail). * fix(resilience): demote v17.1 changelog H4 to bold-paragraph (methodology-lint compat) CI methodology-lint test (T1.8) requires every H4 in docs/methodology/country-resilience-index.mdx to map to a known scorer dimension via HEADING_TO_DIMENSION — that's how the doc-vs- registry parity gate works. The v17.1 changelog sub-section I added (`#### v17.1 — Net-imports denominator parity for liquidReserveAdequacy (U8.1)`) is a non-dimension subsection and tripped the linter. Demoted to a bold lead-in paragraph (`**v17.1 — …**`). Same content, same prose, just no H4. The lint comment explicitly anticipates this case ("if a future edit adds a legitimate non-dimension H4, either upgrade it to H3 or add an explicit allowlist") — bold-paragraph is the cleanest of the three options and matches how Mintlify renders sub-changelog notes elsewhere in the doc. Local re-run: methodology-lint + doc-parity 10/10 pass; full test:data suite 7606/7606 pass, 0 fail. * fix(resilience): plan 002 §U8 review fixes (P1 cache bump v17→v18, P2 doc TTL/dim drift, P2 stale-count tightening) Three review findings + a Greptile P2 perf note. All addressed locally. P1 — cache invalidation missing for the §U8.1 scorer-formula change. liquidReserveAdequacy now adjusts reserveMonths by re-export share for AE/PA, but the prior commit kept score/ranking at v17 and history at v12. The `_formula` tag is binary 'd6'|'pc' and does NOT detect intra- 'd6' scorer changes — without this bump, cached v17 AE/PA scores (gross-imports-denominated) would continue to serve until TTL expiry, defeating the construct fix this PR delivers. Bumped: - RESILIENCE_SCORE_CACHE_PREFIX v17 → v18 (_shared.ts:164) - RESILIENCE_RANKING_CACHE_KEY v17 → v18 (_shared.ts:235) - RESILIENCE_HISTORY_KEY_PREFIX v12 → v13 (_shared.ts:217) - script mirrors: seed-resilience-scores.mjs, backtest, validate, benchmark - api/health.js: resilienceRanking key History bumps in lockstep so the rolling 30-day window doesn't mix pre/post-fix points and manufacture a false "improving" trend on day one. Same pattern as PR 3A's v11→v12 lockstep for the SWF-side fix. P2 — Redis keys table TTL + write-gate prose drift. Doc claimed: resilience:ranking:v17 | 6 hours | "only when all countries are scored" Actual code: 12h TTL (RESILIENCE_RANKING_CACHE_TTL_SECONDS) and publishes when coverage >= RANKING_CACHE_MIN_COVERAGE = 0.75. Updated the table to reflect both. Same correction caught by the parity test that flagged the v11/v17 drift in the prior commit. P2 — stale "19 dimensions" claim still in the doc. The v2.0 scorecard Data row at line 750 still said "19 dimensions across 6 domains" because the parity test only required ONE correct mention. Tightened the test to fail on any "<N> dimensions" mention in the plausible-current-total band [15, 25] that doesn't equal activeCount or totalCount — historical sub-counts (e.g. v1.0's "13 dimensions", recovery pillar's "6 dimensions") are outside the band and stay untouched. Updated the stale line to "20 active dimensions across 6 domains (plus 2 structurally-retired dimensions)". Bonus — addressed a load of test-side cache-key literal duplications (plan 002 §U8 follow-up): - tests/resilience-ranking.test.mts: 27 hardcoded 'resilience:score:v17:XX' / 'resilience:history:v12:XX' literals swapped to ${RESILIENCE_SCORE_CACHE_PREFIX}/${RESILIENCE_HISTORY_KEY_PREFIX} template-literal references. - tests/resilience-handlers.test.mts: same. - tests/resilience-pillar-aggregation.test.mts: pinned-to-version assertion replaced with structural-shape regex. - tests/resilience-scores-seed.test.mjs: same. This was caught by PR koala73#3477 review for the ranking key; the score and history prefixes had the same drift trap that resurfaced now. The proper fix per the project skill (proto-required-field-add-needs-cache- backfill Trap 5b) is to import the constants — never hardcode literals. Greptile P2 (per-country Redis read for reexport-share map) — verified benign: createMemoizedSeedReader caches by key, and _shared.ts:992's `sharedReader` is shared across all 196 countries in the warm path (getOrComputeResilienceScores), so reexport-share is fetched ONCE per ranking compute, not 196 times. Single-country handler creates a fresh memoized reader per request → 1 read per request. No fix needed. Verification: 214/214 resilience tests pass, 7606/7606 npm test:data pass, typecheck clean, lint:md clean.
Summary
Closes the plan 2026-04-26-002 sequence (PRs #3425 → #3477) with U8.
liquidReserveAdequacy. PR feat(resilience): PR 3A — net-imports denominator for sovereignFiscalBuffer #3380 / feat(resilience): Comtrade-backed re-export-share seeder + SWF Redis read #3385 shipped re-export-adjusted denominators forsovereignFiscalBuffervia the SWF seeder. The same correction was structurally needed on the sibling reserves-in-months dimension, which consumes WBFI.RES.TOTL.MO(computed at WB source against gross imports) and over-penalizes hubs. v17.1 multiplies WB's pre-computed months by1 / (1 − reexportShare)for hub countries (today: AE 35.5%, PA similar) — algebraic inverse of dividing the denominator. Non-hubs unchanged. Expected impact at next ranking refresh: AE liquidReserveAdequacy ≈38 → ≈64 (a +26-point dim swing); PA similar.resilience:ranking:v17cohort numbers captured 2026-04-28: median(Nordics)=78.52 vs median(GCC)=70.53 (+7.98pt), min(G7)=64.31 vs max(LIC)=53.73 (+10.58pt), 1 microstate (MO Add related-asset cards to clusters and map highlighting for infrastructure #4) in top-20. Documents 3 known open construct gaps honestly.tests/resilience-doc-parity.test.mts). Asserts the doc's prose claims match_shared.ts(cache prefix constants),RESILIENCE_DOMAIN_ORDER(6 domains), and active-dim count (ORDER − RETIRED= 20). Caught real drift: Redis keys table named v11 prefixes despite live v17, and the headline "19 dimensions" claim was off-by-one after PR 2 §3.4. Both fixed in this PR.formatResilienceConfidencenow renders "Outside headline ranking" whenheadlineEligible: false, distinct from "Low confidence — sparse data" whenlowConfidence: true. Order matters:lowConfidenceis more specific so it wins when both flags fire.Test plan
All checks green from the worktree:
npm run typecheck— cleannpm run lint:md— 0 errorsnpm run lint:boundaries— no violationsnpm run lint:rate-limit-policies— 7 policies cleannpm run lint:premium-fetch— parity cleannpm run version:check— 2.8.0 in synctests/resilience-dimension-scorers.test.mts— 89 tests pass (4 new for U8.1 net-imports adjustment)tests/resilience-widget.test.mts— 36 tests pass (3 new for the eligibility-aware confidence label)tests/resilience-doc-parity.test.mts— 5 tests pass (new file)tests/resilience-headline-eligible-gate.test.mts+tests/resilience-ranking.test.mts— full plan-002 regression suite greentests/handlers.test.mts+tests/server-handlers.test.mjs— 102 tests passtests/edge-functions.test.mjs— clean<digit/lettersweep — cleanPost-merge ops note
No cache-prefix bump (the v17 prefix was just bumped 24h ago in #3469; another bump would force needless rebuild). The §U8.1 formula change for AE + PA flows through on the next ranking refresh (within the 6h cache TTL). Trend metric will show a one-time step at deploy time for these two countries; this is the corrected baseline going forward.
Honest construct gaps documented in the doc
importConcentrationcoverage gap on AE (UN Comtrade falls through tounmonitoredimpute despite data being available) — likely a seeder gap, follow-up.cyberDigitaltransient zeros — high dimension volatility on a 24-72h window. Smoothing-refinement is plan-deferred.