fix(resilience): symmetric headline gate + cache-key constant import in tests (PR #3472 follow-up)#3477
Conversation
…in tests (PR #3472 review round 2) Two Greptile P2s on PR #3472 (now merged): (P2) Asymmetric gate application. The cache-hit gate demoted ineligible items from items[] → greyedOut[], but never promoted items already in greyedOut[] that carry headlineEligible:true. Result: an item that now passes the gate could remain demoted across all cache-hit reads until the next 6h-TTL recompute. The PR's stated contract is "gate is the single source of truth across both code paths," which the asymmetric implementation only half-delivered. Fix: split greyedOut[] symmetrically. Items with `headlineEligible: true` in either array land in items[]; everything else lands in greyedOut[]. The gate is now the only thing that decides bucket placement on the cache-hit path. Added a dedicated regression test ("promotes greyedOut items with headlineEligible:true to items[]") and mutation-verified: removing `promotedFromGreyed` from items[] makes the test fail. (P2) Hardcoded cache key in tests. Two `redis.set('resilience:ranking:v17')` literals in tests/resilience-headline-eligible-gate.test.mts. A future v18 bump would silently land at a different Redis key, the handler finds nothing, falls through to recompute, tests fail for an opaque reason unrelated to gate logic. Replaced with imported `RESILIENCE_RANKING_CACHE_KEY`. (Test fixture update) The pre-existing "all-greyed-out cached payload" test had `headlineEligible: true` on greyedOut items — anomalous state the new symmetric gate now correctly handles by promoting those items to items[]. Updated the fixture to `false`, matching what a legitimate post-PR-6 writer stamps on low-coverage countries. 676/676 resilience tests pass; typecheck + lint clean.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR completes the symmetric headline gate on the cache-hit path — items in Confidence Score: 4/5Safe to merge; both findings are P2 and only affect anomalous cache states or future maintenance friction. Both inline findings are P2: the sort-order issue only materialises in an anomalous scenario explicitly documented as rare, and the incomplete cache-key migration causes future test confusion rather than current runtime breakage. No P1 or P0 issues found. tests/resilience-ranking.test.mts — hardcoded cache-key literals remain throughout; server/worldmonitor/resilience/v1/get-resilience-ranking.ts — items[] not re-sorted after symmetric promotion. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Cache hit] --> B[Universe filter both arrays]
B --> C[backfillEligibilityConservative: missing to false]
C --> D{Split each array by headlineEligible}
D -->|items eligible| E[eligibleItems]
D -->|items ineligible| F[ineligibleFromItems]
D -->|greyedOut eligible| G[promotedFromGreyed]
D -->|greyedOut ineligible| H[stillGreyed]
E --> I[items: eligibleItems + promotedFromGreyed - not re-sorted]
G --> I
H --> J[greyedOut: stillGreyed + ineligibleFromItems]
F --> J
I --> K[Return to caller]
J --> K
Reviews (1): Last reviewed commit: "fix(resilience): symmetric headline gate..." | Re-trigger Greptile |
| { countryCode: 'ER', overallScore: 10, level: 'critical', lowConfidence: true, overallCoverage: 0.12, headlineEligible: false }, | ||
| ], | ||
| }; | ||
| redis.set('resilience:ranking:v17', JSON.stringify({ ...cachedPublic, _formula: 'd6' })); |
There was a problem hiding this comment.
Hardcoded cache key not migrated here
This PR migrated two hardcoded 'resilience:ranking:v17' literals in resilience-headline-eligible-gate.test.mts to RESILIENCE_RANKING_CACHE_KEY, but resilience-ranking.test.mts retains the same pattern across ~10 callsites (lines 64, 90, 125, 203, 250, 303, 350, 399, 474, 566, 620, 710). Line 125 is in the exact block this PR modified (changing headlineEligible values), making the omission particularly visible. A v18 bump will silently route cache-seeding writes to the wrong key, causing tests that should cover the cache-hit path to fall through to the recompute path instead — failing for reasons unrelated to the logic under test.
The same replacement applies to the remaining hardcoded occurrences throughout the file, and RESILIENCE_RANKING_CACHE_KEY will need to be added to the import from _shared.ts at line 5.
| redis.set('resilience:ranking:v17', JSON.stringify({ ...cachedPublic, _formula: 'd6' })); | |
| redis.set(RESILIENCE_RANKING_CACHE_KEY, JSON.stringify({ ...cachedPublic, _formula: 'd6' })); |
| items: [...eligibleItems, ...promotedFromGreyed], | ||
| greyedOut: [...stillGreyed, ...ineligibleFromItems], |
There was a problem hiding this comment.
Promoted items not re-sorted — items[] order may break
When promotedFromGreyed is non-empty (the anomalous-but-now-handled case), items[] becomes [...eligibleItems, ...promotedFromGreyed]. eligibleItems preserves the cached sort order, but promotedFromGreyed items are appended without ranking order, so a high-score promoted item (e.g., NO: 82) could trail a lower-score eligible item (e.g., US: 61) already present in items[]. The fresh-compute path applies sortRankingItems explicitly; the cache-hit path should do the same after combining the two sets.
| items: [...eligibleItems, ...promotedFromGreyed], | |
| greyedOut: [...stillGreyed, ...ineligibleFromItems], | |
| items: sortRankingItems([...eligibleItems, ...promotedFromGreyed]), | |
| greyedOut: [...stillGreyed, ...ineligibleFromItems], |
…ache key in ranking tests (PR #3477 review round 1) Two Greptile P2s on PR #3477 (still open): (P2) Promoted greyedOut entries appended to items[] without re-sort. The recompute path uses sortRankingItems before publishing, but the cache-hit path's symmetric promotion just did `[...eligibleItems, ...promotedFromGreyed]` — a high-scoring item promoted from greyedOut would land at the END of items[] instead of its correct rank position. Cache-hit responses visibly differed from fresh recomputes for the same data, breaking the front-of-house ranking sort. Fix: wrap with sortRankingItems on the cache-hit return. Added a dedicated regression test ("re-sorts items[] after promoting from greyedOut") that seeds items=[US@61], greyedOut=[NO@82, both eligible:true] and asserts the response order is [NO, US]. Mutation- verified: removing the sort wrapper makes the test fail. (P2) Hardcoded cache-key literals in tests/resilience-ranking.test.mts. The previous fix only covered tests/resilience-headline-eligible-gate .test.mts; the sibling file still had 12 `'resilience:ranking:v17'` literals. Same fragility: a v18 bump would silently break tests at the wrong key. Replaced all 12 with the imported `RESILIENCE_RANKING_CACHE_KEY` constant. 677/677 resilience tests pass; typecheck + lint clean.
… 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.
…+ net-imports parity (v17.1) (#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 #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). * 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 #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.
…in tests (PR koala73#3472 follow-up) (koala73#3477) * fix(resilience): symmetric headline gate + cache-key constant import in tests (PR koala73#3472 review round 2) Two Greptile P2s on PR koala73#3472 (now merged): (P2) Asymmetric gate application. The cache-hit gate demoted ineligible items from items[] → greyedOut[], but never promoted items already in greyedOut[] that carry headlineEligible:true. Result: an item that now passes the gate could remain demoted across all cache-hit reads until the next 6h-TTL recompute. The PR's stated contract is "gate is the single source of truth across both code paths," which the asymmetric implementation only half-delivered. Fix: split greyedOut[] symmetrically. Items with `headlineEligible: true` in either array land in items[]; everything else lands in greyedOut[]. The gate is now the only thing that decides bucket placement on the cache-hit path. Added a dedicated regression test ("promotes greyedOut items with headlineEligible:true to items[]") and mutation-verified: removing `promotedFromGreyed` from items[] makes the test fail. (P2) Hardcoded cache key in tests. Two `redis.set('resilience:ranking:v17')` literals in tests/resilience-headline-eligible-gate.test.mts. A future v18 bump would silently land at a different Redis key, the handler finds nothing, falls through to recompute, tests fail for an opaque reason unrelated to gate logic. Replaced with imported `RESILIENCE_RANKING_CACHE_KEY`. (Test fixture update) The pre-existing "all-greyed-out cached payload" test had `headlineEligible: true` on greyedOut items — anomalous state the new symmetric gate now correctly handles by promoting those items to items[]. Updated the fixture to `false`, matching what a legitimate post-PR-6 writer stamps on low-coverage countries. 676/676 resilience tests pass; typecheck + lint clean. * fix(resilience): re-sort items[] after symmetric promotion + import cache key in ranking tests (PR koala73#3477 review round 1) Two Greptile P2s on PR koala73#3477 (still open): (P2) Promoted greyedOut entries appended to items[] without re-sort. The recompute path uses sortRankingItems before publishing, but the cache-hit path's symmetric promotion just did `[...eligibleItems, ...promotedFromGreyed]` — a high-scoring item promoted from greyedOut would land at the END of items[] instead of its correct rank position. Cache-hit responses visibly differed from fresh recomputes for the same data, breaking the front-of-house ranking sort. Fix: wrap with sortRankingItems on the cache-hit return. Added a dedicated regression test ("re-sorts items[] after promoting from greyedOut") that seeds items=[US@61], greyedOut=[NO@82, both eligible:true] and asserts the response order is [NO, US]. Mutation- verified: removing the sort wrapper makes the test fail. (P2) Hardcoded cache-key literals in tests/resilience-ranking.test.mts. The previous fix only covered tests/resilience-headline-eligible-gate .test.mts; the sibling file still had 12 `'resilience:ranking:v17'` literals. Same fragility: a v18 bump would silently break tests at the wrong key. Replaced all 12 with the imported `RESILIENCE_RANKING_CACHE_KEY` constant. 677/677 resilience tests pass; typecheck + lint 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
Follow-up to #3472 — Greptile review on the now-merged PR surfaced two P2s that warrant a separate fix:
P2 — Asymmetric gate application
The cache-hit gate in
get-resilience-ranking.tsdemoted ineligible items fromitems[] → greyedOut[], but never re-evaluated items already sitting in cachedgreyedOut[]. An item flaggedheadlineEligible: truein greyedOut[] would remain demoted on every cache-hit read until a full 6h-TTL recompute. The PR's stated contract — "gate is the single source of truth across both code paths" — was only half-delivered.Fix: split
greyedOut[]symmetrically. Eligible items in either array land initems[]; the rest land ingreyedOut[]. New regression test"promotes greyedOut items with headlineEligible:true to items[] (symmetric gate)". Mutation-verified: removingpromotedFromGreyedfrom items[] makes the test fail.P2 — Hardcoded cache key in tests
Two
redis.set('resilience:ranking:v17', ...)literals intests/resilience-headline-eligible-gate.test.mts. A future v18 bump would silently land at a different Redis key, the handler finds nothing, falls through to recompute, and tests fail for an opaque reason unrelated to gate logic. Replaced with importedRESILIENCE_RANKING_CACHE_KEY.Test fixture cleanup
The pre-existing "all-greyed-out cached payload" test had
headlineEligible: trueon items ingreyedOut[]— exactly the anomalous state the new symmetric gate correctly handles by promotion. Updated the fixture tofalseto match what a legitimate post-PR-6 writer stamps on low-coverage countries.Test plan
npm run typecheckcleannpm run lintclean (exit 0)References
docs/plans/2026-04-26-002-feat-resilience-universe-coverage-rebuild-plan.md