feat(resilience): T1.7 source-failure wiring + delete absence branch#2964
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR wires Phase 1 T1.7 source-failure detection: after the 13 dimension scorers run, All remaining findings are P2 (stale inline comments, a misleading test name, a missing explanatory comment at the decoration site about the no-data-at-all gap, and a wrong PR number in the plan doc); none block merge. Confidence Score: 5/5Safe to merge; all findings are P2 (stale comments, a misleading test name, and a wrong PR number in docs). The core logic is sound and well-tested: readFailedDatasets is defensive, DATASET_TO_DIMENSIONS covers all 11 seed adapters with a compile-time parity test, and scoreAllDimensions correctly guards re-tagging behind imputationClass != null. The known "no-data gap" for fully-observed-but-failed dimensions is intentional Phase 1 scope and explicitly documented in the test. No P0/P1 findings. _dimension-scorers.ts has two stale comment blocks (lines 71–87 and 1188–1197) that should be updated before this file is next touched. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[scoreAllDimensions called] --> B{Run 13 scorers + readFailedDatasets in parallel}
B --> C[13 ResilienceDimensionScore entries]
B --> D[failedDatasets: string array]
D --> E{failedDatasets.length > 0?}
E -- No --> Z[Return scores as-is]
E -- Yes --> F[failedDimensionsFromDatasets\nDATASET_TO_DIMENSIONS lookup]
F --> G{For each affected dimension}
G --> H{current.imputationClass != null?}
H -- No\nno data / real data --> I[Keep existing null class]
H -- Yes\nfully imputed unmonitored/stable-absence --> J[Overwrite to source-failure\nconsole.info log]
I --> Z
J --> Z
|
| for (const dimId of affected) { | ||
| const current = scores[dimId]; | ||
| // Only re-tag imputed dimensions. Dimensions with any observed | ||
| // weight keep their existing null class (which is the correct | ||
| // semantics: the seed failing did not prevent us from producing | ||
| // a real-data score for this country). | ||
| if (current != null && current.imputationClass != null) { | ||
| scores[dimId] = { ...current, imputationClass: 'source-failure' }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Source-failure decoration silently skips "no data at all" dimensions
Dimensions like governanceInstitutional (WGI-only), healthPublicService (WHO-only), and energy have no imputation fallback in their scorer — they call weightedBlend with all-null metrics when the adapter is down, which returns imputationClass: null. The decoration then skips them because current.imputationClass != null is false.
Consequence: when wgi is in failedDatasets, a country that has no WGI data (or whose prior snapshot was purged) keeps governanceInstitutional.imputationClass === null instead of source-failure. Consumers cannot distinguish "adapter never covered this country" from "adapter failed" for these dimensions.
The first integration test documents this limitation inline, but there is no comment at the decoration site explaining why the check is != null rather than also re-tagging the (observedWeight === 0 && imputedWeight === 0) case. Adding a brief comment here would make the gap explicit for future maintainers.
| it('re-tags imputed dimensions when their adapter is in failedDatasets', async () => { | ||
| // Case: WGI adapter failed at seed time AND the country has no real | ||
| // WGI data in the static record. governanceInstitutional is fully | ||
| // imputed (observedWeight === 0) → must flip from its default class | ||
| // to source-failure. macroFiscal depends on a different data path | ||
| // (IMF + debt) so it stays observed and is NOT re-tagged even | ||
| // though it is in the wgi→dimensions affected set. | ||
| const reader = makeOverrideReader({ | ||
| 'resilience:static:US': { | ||
| // wgi key omitted → scoreGovernanceInstitutional sees no data | ||
| infrastructure: { | ||
| indicators: { | ||
| 'EG.ELC.ACCS.ZS': { value: 100, year: 2025 }, | ||
| 'IS.ROD.PAVE.ZS': { value: 74, year: 2025 }, | ||
| 'EG.USE.ELEC.KH.PC': { value: 12000, year: 2025 }, | ||
| 'IT.NET.BBND.P2': { value: 35, year: 2025 }, | ||
| }, | ||
| }, | ||
| gpi: { score: 2.4, rank: 132, year: 2025 }, | ||
| rsf: { score: 30, rank: 45, year: 2025 }, | ||
| who: { | ||
| indicators: { | ||
| hospitalBeds: { value: 2.8, year: 2024 }, | ||
| uhcIndex: { value: 82, year: 2024 }, | ||
| measlesCoverage: { value: 91, year: 2024 }, | ||
| physiciansPer1k: { value: 2.6, year: 2024 }, | ||
| healthExpPerCapitaUsd: { value: 12000, year: 2024 }, | ||
| }, | ||
| }, | ||
| fao: { peopleInCrisis: 5000, phase: 'IPC Phase 2', year: 2025 }, | ||
| aquastat: { indicator: 'Renewable water availability', value: 1500, year: 2024 }, | ||
| iea: { energyImportDependency: { value: 25, year: 2024, source: 'IEA' } }, | ||
| tradeToGdp: { source: 'worldbank', tradeToGdpPct: 25, year: 2023 }, | ||
| fxReservesMonths: { source: 'worldbank', months: 2.5, year: 2023 }, | ||
| appliedTariffRate: { source: 'worldbank', value: 3.5, year: 2023 }, | ||
| }, | ||
| 'seed-meta:resilience:static': { | ||
| fetchedAt: 1712102400000, | ||
| recordCount: 196, | ||
| failedDatasets: ['wgi'], | ||
| }, | ||
| }); | ||
| const dims = await scoreAllDimensions('US', reader); | ||
| // governanceInstitutional is fully imputed (no WGI) → coverage=0, | ||
| // score=0, imputationClass=null from weightedBlend. Even with the | ||
| // source-failure set, it stays null because the decoration only | ||
| // re-tags when imputationClass was already non-null. To exercise | ||
| // the real re-tagging branch, tradeSanctions is the right target: | ||
| // it has a WTO imputation fallback, and we put tradeToGdp into the | ||
| // failed set below in the next test case. For this test, simply | ||
| // assert the infrastructure row (in wgi's affected set only through | ||
| // the logistics mapping) stays correct: the decoration does not | ||
| // touch dimensions that produced real-data scores. | ||
| assert.equal(dims.infrastructure.imputationClass, null, | ||
| 'real-data infrastructure must not be re-tagged even if its adapter is failed'); | ||
| }); |
There was a problem hiding this comment.
Test description doesn't match what it actually asserts
The test is named "re-tags imputed dimensions when their adapter is in failedDatasets" but the only assertion made is that real-data infrastructure is not re-tagged. The actual re-tagging behavior is verified in the next test via tradeSanctions. The test comment itself (lines 989–994) explicitly acknowledges the re-tagging doesn't fire for governanceInstitutional due to the no-data case.
Consider renaming to "does not re-tag real-data dimensions when their adapter is in failedDatasets" to match what is actually asserted.
| ResilienceDimension proto). T1.7 source-failure wiring shipped in | ||
| PR #2964 (consult seed-meta failedDatasets and re-tag affected | ||
| dimensions at the aggregation layer + delete the one remaining |
…P1) Greptile P1 finding on PR #2961: readFreshnessMap() assumed every INDICATOR_REGISTRY sourceKey could be fetched as seed-meta:<sourceKey>, but most entries use placeholder templates like resilience:static:{ISO2}, energy:mix:v1:{ISO2}, and displacement:summary:v1:{year}. Those produce literal lookups like seed-meta:resilience:static:{ISO2} which don't exist in Redis, so the freshness map missed every templated entry and classifyDimensionFreshness marked the affected dimensions stale even with healthy seeds. Most Phase 1 T1.5 freshness badges were broken on arrival. Fix: two-layer resolution in _dimension-freshness.ts. Layer 1 stripTemplateTokens: drop :{placeholder} and :* segments. 'resilience:static:{ISO2}' -> 'resilience:static' 'resilience:static:*' -> 'resilience:static' 'energy:mix:v1:{ISO2}' -> 'energy:mix:v1' 'displacement:summary:v1:{year}' -> 'displacement:summary:v1' Layer 2 stripTrailingVersion: strip trailing :v\d+, mirroring writeExtraKeyWithMeta + runSeed() in scripts/_seed-utils.mjs which never persist the trailing version in seed-meta keys. Handles cyber:threats:v2, infra:outages:v1, unrest:events:v1, conflict:ucdp-events:v1, sanctions:country-counts:v1, and the displacement v1 case above. Layer 3 SOURCE_KEY_META_OVERRIDES: explicit table for drift cases where the two strips still do not match the real seed-meta key. Verified against api/seed-health.js, api/health.js, and scripts/seed-*. Drift cases covered: economic:imf:macro -> economic:imf-macro economic:bis:eer -> economic:bis economic:energy:v1:all -> economic:energy-prices energy:mix -> economic:owid-energy-mix energy:gas-storage -> energy:gas-storage-countries news:threat:summary -> news:threat-summary intelligence:social:reddit -> intelligence:social-reddit readFreshnessMap now deduplicates reads by resolved meta key (so the 15+ resilience:static indicators share one Redis read) and projects per-meta-key results back onto per-sourceKey map entries so classifyDimensionFreshness can keep its existing interface. Regression coverage: - stripTemplateTokens cases for {ISO2}, {year}, and *. - stripTrailingVersion cases for :v1 / :v2 suffixes. - Embedded :v1 carve-out (trade:restrictions:v1:tariff-overview:50 stays unchanged because :v1 is not trailing). - Override cases for the seven drift entries. - Integration test that proves every resilience:static:* / {ISO2} registry entry resolves to the same seed-meta and is marked fresh when that one key has a recent fetchedAt. - healthPublicService end-to-end test: classifies fresh when seed-meta:resilience:static is recent (was stale before the fix). - Registry-coverage assertion: every INDICATOR_REGISTRY sourceKey must resolve to a seed-meta key that either lives in api/seed-health.js, api/health.js, or the test's KNOWN_SEEDS_NOT_IN_HEALTH allowlist (which covers the four seeds written by writeExtraKeyWithMeta / runSeed that no health monitor tracks yet: trade:restrictions, trade:barriers, sanctions:country-counts, economic:energy-prices). Fails loudly if a future registry entry introduces an unknown sourceKey. Note on P1 #2 (scoreCurrencyExternal absence-branch delete): that is PR #2964's scope (T1.7 source-failure wiring), not #2961 (T1.5 propagation pass). #2961 never claimed to delete the fallback branch; no test in this branch expects the new IMPUTE.bisEer fallback. The reviewer conflated the two stacked PRs. #2964 owns the delete.
|
This finding is against pre-fix state. The legacy Actual current code on if (hasReserves) {
const coverage = bisExchangeRaw != null ? 0.4 : 0.3;
return { score: reservesScore, coverage, observedWeight: 1, imputedWeight: 0, imputationClass: null };
}
// No BIS EER, no IMF inflation fallback, no WB reserves fallback.
// This is true structural absence: the country isn't covered by any
// currency-stability source we track. Tag with curated_list_absent
// (= 'unmonitored') so the taxonomy is the single source of truth
// and the aggregation pass can still re-tag it as 'source-failure'
// when the underlying adapter fails. The prior absence-based branch
// returned { score: 50, imputationClass: null } which silently
// bypassed the taxonomy; replaced in T1.7 source-failure wiring.
return {
score: IMPUTE.bisEer.score,
coverage: IMPUTE.bisEer.certaintyCoverage,
observedWeight: 0,
imputedWeight: 1,
imputationClass: IMPUTE.bisEer.imputationClass,
};The Evidence the fix is on the branch: Test coverage: If your review tool is diffing against a snapshot older than |
…P1) Greptile P1 finding on PR #2961: readFreshnessMap() assumed every INDICATOR_REGISTRY sourceKey could be fetched as seed-meta:<sourceKey>, but most entries use placeholder templates like resilience:static:{ISO2}, energy:mix:v1:{ISO2}, and displacement:summary:v1:{year}. Those produce literal lookups like seed-meta:resilience:static:{ISO2} which don't exist in Redis, so the freshness map missed every templated entry and classifyDimensionFreshness marked the affected dimensions stale even with healthy seeds. Most Phase 1 T1.5 freshness badges were broken on arrival. Fix: two-layer resolution in _dimension-freshness.ts. Layer 1 stripTemplateTokens: drop :{placeholder} and :* segments. 'resilience:static:{ISO2}' -> 'resilience:static' 'resilience:static:*' -> 'resilience:static' 'energy:mix:v1:{ISO2}' -> 'energy:mix:v1' 'displacement:summary:v1:{year}' -> 'displacement:summary:v1' Layer 2 stripTrailingVersion: strip trailing :v\d+, mirroring writeExtraKeyWithMeta + runSeed() in scripts/_seed-utils.mjs which never persist the trailing version in seed-meta keys. Handles cyber:threats:v2, infra:outages:v1, unrest:events:v1, conflict:ucdp-events:v1, sanctions:country-counts:v1, and the displacement v1 case above. Layer 3 SOURCE_KEY_META_OVERRIDES: explicit table for drift cases where the two strips still do not match the real seed-meta key. Verified against api/seed-health.js, api/health.js, and scripts/seed-*. Drift cases covered: economic:imf:macro -> economic:imf-macro economic:bis:eer -> economic:bis economic:energy:v1:all -> economic:energy-prices energy:mix -> economic:owid-energy-mix energy:gas-storage -> energy:gas-storage-countries news:threat:summary -> news:threat-summary intelligence:social:reddit -> intelligence:social-reddit readFreshnessMap now deduplicates reads by resolved meta key (so the 15+ resilience:static indicators share one Redis read) and projects per-meta-key results back onto per-sourceKey map entries so classifyDimensionFreshness can keep its existing interface. Regression coverage: - stripTemplateTokens cases for {ISO2}, {year}, and *. - stripTrailingVersion cases for :v1 / :v2 suffixes. - Embedded :v1 carve-out (trade:restrictions:v1:tariff-overview:50 stays unchanged because :v1 is not trailing). - Override cases for the seven drift entries. - Integration test that proves every resilience:static:* / {ISO2} registry entry resolves to the same seed-meta and is marked fresh when that one key has a recent fetchedAt. - healthPublicService end-to-end test: classifies fresh when seed-meta:resilience:static is recent (was stale before the fix). - Registry-coverage assertion: every INDICATOR_REGISTRY sourceKey must resolve to a seed-meta key that either lives in api/seed-health.js, api/health.js, or the test's KNOWN_SEEDS_NOT_IN_HEALTH allowlist (which covers the four seeds written by writeExtraKeyWithMeta / runSeed that no health monitor tracks yet: trade:restrictions, trade:barriers, sanctions:country-counts, economic:energy-prices). Fails loudly if a future registry entry introduces an unknown sourceKey. Note on P1 #2 (scoreCurrencyExternal absence-branch delete): that is PR #2964's scope (T1.7 source-failure wiring), not #2961 (T1.5 propagation pass). #2961 never claimed to delete the fallback branch; no test in this branch expects the new IMPUTE.bisEer fallback. The reviewer conflated the two stacked PRs. #2964 owns the delete.
b835102 to
a43acd2
Compare
…ass) (#2961) * feat(resilience): dimension freshness propagation (T1.5 propagation pass) Ships the Phase 1 T1.5 propagation pass of the country-resilience reference-grade upgrade plan. PR #2947 shipped the staleness classifier foundation (classifyStaleness, cadence taxonomy, three staleness levels) and explicitly deferred the dimension-level propagation. This PR consumes the classifier and surfaces per dimension freshness on the ResilienceDimension response. What this PR commits - Proto: new DimensionFreshness message + `freshness` field on ResilienceDimension (last_observed_at_ms, staleness string). - New module server/worldmonitor/resilience/v1/_dimension-freshness.ts that reads seed-meta values for every sourceKey in INDICATOR_REGISTRY and aggregates the worst staleness + oldest fetchedAt across the constituent indicators of each dimension. - scoreAllDimensions decorates each dimension score with its freshness result before returning. The 13 dimension scorer function bodies are untouched: aggregation is a decoration pass at the caller level so this PR stays mechanical. - Response builder: _shared.ts buildDimensionList propagates the freshness field to the proto output. - Tests: 10 classifyDimensionFreshness + readFreshnessMap cases in a new test file + response-shape case on the release-gate test. Aggregation rules - last_observed_at_ms: MIN fetchedAt across the dimension's indicators (oldest signal = most conservative bound). 0 when no signal has ever been observed. - staleness: MAX staleness level across the dimension's indicators (stale > aging > fresh). Empty string when the dimension has no indicators in the registry (defensive path). What is deliberately NOT in this PR - No changes to the 13 individual dimension scorer function bodies. Per-signal freshness inside scorers is a future enhancement. - No widget rendering of the freshness badge (T1.6 full grid, PR 3). - No cache key bump: additive int64/string fields with zero defaults. Verified - make generate clean, new interface in regenerated types - typecheck + typecheck:api clean - tests/resilience-dimension-freshness.test.mts all new cases pass - tests/resilience-*.test.mts full suite pass - test:data clean - lint exits 0 on touched files * fix(resilience): resolve templated sourceKeys to real seed-meta (#2961 P1) Greptile P1 finding on PR #2961: readFreshnessMap() assumed every INDICATOR_REGISTRY sourceKey could be fetched as seed-meta:<sourceKey>, but most entries use placeholder templates like resilience:static:{ISO2}, energy:mix:v1:{ISO2}, and displacement:summary:v1:{year}. Those produce literal lookups like seed-meta:resilience:static:{ISO2} which don't exist in Redis, so the freshness map missed every templated entry and classifyDimensionFreshness marked the affected dimensions stale even with healthy seeds. Most Phase 1 T1.5 freshness badges were broken on arrival. Fix: two-layer resolution in _dimension-freshness.ts. Layer 1 stripTemplateTokens: drop :{placeholder} and :* segments. 'resilience:static:{ISO2}' -> 'resilience:static' 'resilience:static:*' -> 'resilience:static' 'energy:mix:v1:{ISO2}' -> 'energy:mix:v1' 'displacement:summary:v1:{year}' -> 'displacement:summary:v1' Layer 2 stripTrailingVersion: strip trailing :v\d+, mirroring writeExtraKeyWithMeta + runSeed() in scripts/_seed-utils.mjs which never persist the trailing version in seed-meta keys. Handles cyber:threats:v2, infra:outages:v1, unrest:events:v1, conflict:ucdp-events:v1, sanctions:country-counts:v1, and the displacement v1 case above. Layer 3 SOURCE_KEY_META_OVERRIDES: explicit table for drift cases where the two strips still do not match the real seed-meta key. Verified against api/seed-health.js, api/health.js, and scripts/seed-*. Drift cases covered: economic:imf:macro -> economic:imf-macro economic:bis:eer -> economic:bis economic:energy:v1:all -> economic:energy-prices energy:mix -> economic:owid-energy-mix energy:gas-storage -> energy:gas-storage-countries news:threat:summary -> news:threat-summary intelligence:social:reddit -> intelligence:social-reddit readFreshnessMap now deduplicates reads by resolved meta key (so the 15+ resilience:static indicators share one Redis read) and projects per-meta-key results back onto per-sourceKey map entries so classifyDimensionFreshness can keep its existing interface. Regression coverage: - stripTemplateTokens cases for {ISO2}, {year}, and *. - stripTrailingVersion cases for :v1 / :v2 suffixes. - Embedded :v1 carve-out (trade:restrictions:v1:tariff-overview:50 stays unchanged because :v1 is not trailing). - Override cases for the seven drift entries. - Integration test that proves every resilience:static:* / {ISO2} registry entry resolves to the same seed-meta and is marked fresh when that one key has a recent fetchedAt. - healthPublicService end-to-end test: classifies fresh when seed-meta:resilience:static is recent (was stale before the fix). - Registry-coverage assertion: every INDICATOR_REGISTRY sourceKey must resolve to a seed-meta key that either lives in api/seed-health.js, api/health.js, or the test's KNOWN_SEEDS_NOT_IN_HEALTH allowlist (which covers the four seeds written by writeExtraKeyWithMeta / runSeed that no health monitor tracks yet: trade:restrictions, trade:barriers, sanctions:country-counts, economic:energy-prices). Fails loudly if a future registry entry introduces an unknown sourceKey. Note on P1 #2 (scoreCurrencyExternal absence-branch delete): that is PR #2964's scope (T1.7 source-failure wiring), not #2961 (T1.5 propagation pass). #2961 never claimed to delete the fallback branch; no test in this branch expects the new IMPUTE.bisEer fallback. The reviewer conflated the two stacked PRs. #2964 owns the delete.
…2962) * feat(resilience): dimension freshness propagation (T1.5 propagation pass) Ships the Phase 1 T1.5 propagation pass of the country-resilience reference-grade upgrade plan. PR #2947 shipped the staleness classifier foundation (classifyStaleness, cadence taxonomy, three staleness levels) and explicitly deferred the dimension-level propagation. This PR consumes the classifier and surfaces per dimension freshness on the ResilienceDimension response. What this PR commits - Proto: new DimensionFreshness message + `freshness` field on ResilienceDimension (last_observed_at_ms, staleness string). - New module server/worldmonitor/resilience/v1/_dimension-freshness.ts that reads seed-meta values for every sourceKey in INDICATOR_REGISTRY and aggregates the worst staleness + oldest fetchedAt across the constituent indicators of each dimension. - scoreAllDimensions decorates each dimension score with its freshness result before returning. The 13 dimension scorer function bodies are untouched: aggregation is a decoration pass at the caller level so this PR stays mechanical. - Response builder: _shared.ts buildDimensionList propagates the freshness field to the proto output. - Tests: 10 classifyDimensionFreshness + readFreshnessMap cases in a new test file + response-shape case on the release-gate test. Aggregation rules - last_observed_at_ms: MIN fetchedAt across the dimension's indicators (oldest signal = most conservative bound). 0 when no signal has ever been observed. - staleness: MAX staleness level across the dimension's indicators (stale > aging > fresh). Empty string when the dimension has no indicators in the registry (defensive path). What is deliberately NOT in this PR - No changes to the 13 individual dimension scorer function bodies. Per-signal freshness inside scorers is a future enhancement. - No widget rendering of the freshness badge (T1.6 full grid, PR 3). - No cache key bump: additive int64/string fields with zero defaults. Verified - make generate clean, new interface in regenerated types - typecheck + typecheck:api clean - tests/resilience-dimension-freshness.test.mts all new cases pass - tests/resilience-*.test.mts full suite pass - test:data clean - lint exits 0 on touched files * fix(resilience): resolve templated sourceKeys to real seed-meta (#2961 P1) Greptile P1 finding on PR #2961: readFreshnessMap() assumed every INDICATOR_REGISTRY sourceKey could be fetched as seed-meta:<sourceKey>, but most entries use placeholder templates like resilience:static:{ISO2}, energy:mix:v1:{ISO2}, and displacement:summary:v1:{year}. Those produce literal lookups like seed-meta:resilience:static:{ISO2} which don't exist in Redis, so the freshness map missed every templated entry and classifyDimensionFreshness marked the affected dimensions stale even with healthy seeds. Most Phase 1 T1.5 freshness badges were broken on arrival. Fix: two-layer resolution in _dimension-freshness.ts. Layer 1 stripTemplateTokens: drop :{placeholder} and :* segments. 'resilience:static:{ISO2}' -> 'resilience:static' 'resilience:static:*' -> 'resilience:static' 'energy:mix:v1:{ISO2}' -> 'energy:mix:v1' 'displacement:summary:v1:{year}' -> 'displacement:summary:v1' Layer 2 stripTrailingVersion: strip trailing :v\d+, mirroring writeExtraKeyWithMeta + runSeed() in scripts/_seed-utils.mjs which never persist the trailing version in seed-meta keys. Handles cyber:threats:v2, infra:outages:v1, unrest:events:v1, conflict:ucdp-events:v1, sanctions:country-counts:v1, and the displacement v1 case above. Layer 3 SOURCE_KEY_META_OVERRIDES: explicit table for drift cases where the two strips still do not match the real seed-meta key. Verified against api/seed-health.js, api/health.js, and scripts/seed-*. Drift cases covered: economic:imf:macro -> economic:imf-macro economic:bis:eer -> economic:bis economic:energy:v1:all -> economic:energy-prices energy:mix -> economic:owid-energy-mix energy:gas-storage -> energy:gas-storage-countries news:threat:summary -> news:threat-summary intelligence:social:reddit -> intelligence:social-reddit readFreshnessMap now deduplicates reads by resolved meta key (so the 15+ resilience:static indicators share one Redis read) and projects per-meta-key results back onto per-sourceKey map entries so classifyDimensionFreshness can keep its existing interface. Regression coverage: - stripTemplateTokens cases for {ISO2}, {year}, and *. - stripTrailingVersion cases for :v1 / :v2 suffixes. - Embedded :v1 carve-out (trade:restrictions:v1:tariff-overview:50 stays unchanged because :v1 is not trailing). - Override cases for the seven drift entries. - Integration test that proves every resilience:static:* / {ISO2} registry entry resolves to the same seed-meta and is marked fresh when that one key has a recent fetchedAt. - healthPublicService end-to-end test: classifies fresh when seed-meta:resilience:static is recent (was stale before the fix). - Registry-coverage assertion: every INDICATOR_REGISTRY sourceKey must resolve to a seed-meta key that either lives in api/seed-health.js, api/health.js, or the test's KNOWN_SEEDS_NOT_IN_HEALTH allowlist (which covers the four seeds written by writeExtraKeyWithMeta / runSeed that no health monitor tracks yet: trade:restrictions, trade:barriers, sanctions:country-counts, economic:energy-prices). Fails loudly if a future registry entry introduces an unknown sourceKey. Note on P1 #2 (scoreCurrencyExternal absence-branch delete): that is PR #2964's scope (T1.7 source-failure wiring), not #2961 (T1.5 propagation pass). #2961 never claimed to delete the fallback branch; no test in this branch expects the new IMPUTE.bisEer fallback. The reviewer conflated the two stacked PRs. #2964 owns the delete. * feat(resilience): T1.6 full grid with imputation + freshness columns Ships the Phase 1 T1.6 full grid slice of the country-resilience reference-grade upgrade plan. PR #2949 shipped the per-dimension confidence grid scaffold (label + coverage bar + percentage) and explicitly deferred two columns because their proto fields had not landed yet. PR #2959 (imputationClass) and #2961 (freshness) added those fields to the ResilienceDimension response type. This PR wires both into the widget, completing the grid and satisfying the Phase 1 acceptance criterion "Widget shows per-dimension coverage + imputation class + freshness badge". What this PR commits - DimensionConfidence type gains imputationClass and staleness plus a numeric lastObservedAtMs coerced from the proto int64 string. - formatDimensionConfidence normalizes empty string, unknown, and missing fields into typed nulls so downstream rendering can branch safely. - New helpers getImputationClassIcon, getImputationClassLabel, getStalenessLabel for compact glyphs and tooltip strings. - ResilienceWidget.renderDimensionConfidenceCell renders two new columns: * imputation-icon column between the coverage bar and the percentage (color-coded per class, empty when null) * freshness-dot column at the far right (green/yellow/red dot per staleness, empty when null) aria-label and title attributes explain each glyph for a11y. - country-deep-dive.css: cell grid template expanded from 3 columns to 5. New .resilience-widget__dimension-imputation and .resilience-widget__dimension-freshness rules with per-class and per-level color modifiers. Mobile media-query adjusted to keep the 5-column layout readable below 480px. - LOCKED_PREVIEW fixture populated with a realistic mix of classes and staleness levels so the gated-UI preview shows off the new columns. - Tests: normalization cases for imputationClass and freshness, glyph / label helpers, LOCKED_PREVIEW smoke test. What is deliberately NOT in this PR - No scorer changes. source-failure class is exposed for rendering but no scorer path emits it yet; PR 4 of 5 wires the seed-meta failedDatasets consultation. - formatResilienceConfidence single-label fallback is untouched; the cleanup is out of scope. - No new SVG assets; text glyphs and CSS colors only. Verified - typecheck + typecheck:api clean - tests/resilience-widget-utils.test.mts passing - tests/resilience-*.test.mts full suite passing - test:data clean - lint exit 0
Ships the final slice of Phase 1 T1.7. The foundation (PR #2944) tagged every imputation with a four-class taxonomy at definition time and explicitly deferred the source-failure wiring as a follow-up. PR #2959 exposed the imputationClass field on the ResilienceDimension response. This PR consults the seed-meta failedDatasets array and re-tags imputed dimensions as source-failure when the underlying adapter fetch failed. What this PR commits - New module server/worldmonitor/resilience/v1/_source-failure.ts: readFailedDatasets(reader) reads seed-meta:resilience:static, extracts failedDatasets string[], defensive against missing, null, malformed, non-string entries, and reader throws. failedDimensionsFromDatasets(keys) expands adapter keys into the ResilienceDimensionId set via DATASET_TO_DIMENSIONS. The map is 1-to-1 with the 11 adapters in scripts/seed-resilience-static.mjs fetchAllDatasetMaps(). - scoreAllDimensions decorates the per-dimension score Record after the 13 scorers run: for each dimension in the affected set, if imputationClass != null (i.e. already imputed), overwrite to 'source-failure'. Real-data dimensions keep their existing null class. - Info-level log at the top of the decoration pass when failedDatasets.length > 0, listing failed adapter keys + affected dimensions so ops can see which adapters went down without combing through Redis. - scoreCurrencyExternal: deleted the old absence-based branch that returned imputationClass=null when BIS EER + IMF inflation + WB reserves are all absent. Replaced with an IMPUTE.bisEer fall-through (curated_list_absent, unmonitored) so the taxonomy is the single source of truth. This is the last place in the scorer where a null imputationClass escaped on an imputed return path. - 13 new unit tests in tests/resilience-source-failure.test.mts covering readFailedDatasets defensive branches, failedDimensionsFromDatasets expansion and deduplication, and DATASET_TO_DIMENSIONS coverage against the seed adapter list. - 5 new integration cases in tests/resilience-dimension-scorers.test.mts inside a new describe('resilience source-failure aggregation (T1.7)') block: end-to-end source-failure propagation via tradeSanctions, real-data countries not re-tagged, unaffected dimensions untouched, healthy-seed no-op path. - Updated tests/resilience-scorers.test.mts BIS-null assertion: the "coverage=0 when all seeds missing" contract now carries an explicit exemption for currencyExternal because the legacy null return path is gone (replaced with the taxonomy fall-through). The exemption verifies the new tag is unmonitored. - tests/resilience-dimension-scorers.test.mts 'both BIS and IMF null' case updated to assert the new curated_list_absent semantics. Phase 1 acceptance delta '4-class imputation taxonomy live end-to-end; old absence-based path deleted' now has all four classes emittable and every imputed return path carries a non-null imputationClass. What is deliberately NOT in this PR - No widget rendering changes (PR #2962 already renders the source-failure icon). - No new adapters to DATASET_TO_DIMENSIONS beyond the current 11. - No INDICATOR_REGISTRY sourceKey renames; that is a separate cleanup. - No cache key bump (response shape unchanged). Verified - npm run typecheck and typecheck:api clean - tests/resilience-source-failure.test.mts 13/13 passing - tests/resilience-dimension-scorers.test.mts 63/63 passing - full resilience suite (tests/resilience-*.test.mts + .mjs) passing - npm run test:data 4379/4379 passing - npm run lint exit 0
a43acd2 to
39835fb
Compare
Summary
Phase 1 T1.7 source-failure wiring. PR #2944 tagged every imputation with a four-class taxonomy at definition time and explicitly deferred the source-failure wiring. PR #2959 exposed the
imputationClassfield on the response. This PR consults the existingseed-meta:resilience:static.failedDatasetsarray and re-tags imputed dimensions assource-failurewhen the underlying adapter fetch failed, satisfying the Phase 1 acceptance criterion "4-class imputation taxonomy live end-to-end; old absence-based path deleted".Depends on #2959
Base branch is
fix/t1-7-schema-pass-imputation-class, notmain. Does NOT depend on PR #2961 or #2962; once #2959 lands, this PR can merge independently of the freshness/widget stack.What this PR commits
New module
server/worldmonitor/resilience/v1/_source-failure.tsreadFailedDatasets(reader): readsseed-meta:resilience:static, extractsfailedDatasets: string[], defensive against missing, null, malformed, non-string entries, and reader throws. Always returns an array.failedDimensionsFromDatasets(keys): expands adapter keys into aSet<ResilienceDimensionId>via the explicitDATASET_TO_DIMENSIONSmap. The map is 1-to-1 with the 11 adapters inscripts/seed-resilience-static.mjs::fetchAllDatasetMaps()(wgi, infrastructure, gpi, rsf, who, fao, aquastat, iea, tradeToGdp, fxReservesMonths, appliedTariffRate).scoreAllDimensionsdecoration passAfter the 13 scorers run, if
failedDatasets.length > 0:imputationClass != null(already imputed), overwrite to'source-failure'. Real-data dimensions withobservedWeight > 0keep their existingnullclass.Deleted the remaining absence-based branch
scoreCurrencyExternalhad one return path that emittedimputationClass: nullwithscore: 50, coverage: 0when BIS EER + IMF inflation + WB reserves were ALL absent. That was the last place in the scorer where an untagged null could escape on a non-real-data path. Replaced with anIMPUTE.bisEerfall-through (which aliasesIMPUTATION.curated_list_absent->unmonitored) so the taxonomy is the single source of truth and the aggregation pass can still re-tag tosource-failurewhen the adapter fails.Tests
tests/resilience-source-failure.test.mtsforreadFailedDatasetsdefensive branches,failedDimensionsFromDatasetsexpansion and dedup, andDATASET_TO_DIMENSIONScoverage vs the seed adapter list (breaks loudly if the seed grows an adapter without updating the map).tests/resilience-dimension-scorers.test.mtsunder a newdescribe('resilience source-failure aggregation (T1.7)')block: end-to-end source-failure propagation via the tradeSanctions path, real-data countries not re-tagged, unaffected dimensions untouched, healthy-seed no-op.tests/resilience-scorers.test.mts"all seeds missing" contract: currencyExternal now carries an explicit exemption because the legacy null return path is gone. The exemption asserts the new curated_list_absent tag.tests/resilience-dimension-scorers.test.mts"both BIS and IMF null" case to assert the new curated_list_absent semantics (observedWeight=0, imputedWeight=1, imputationClass='unmonitored').What is NOT in this PR
source-failureicon; the scaffolding was ready before the scorer emitted the class.DATASET_TO_DIMENSIONS. If a new dataset lands in the seed, its mapping lands in the same PR.INDICATOR_REGISTRYsourceKey renames -- naming drift between_indicator-registry.tssourceKeys and the static seed adapter keys is a known wart, separate cleanup.failedDatasetscorrectly.Phase 1 dependency chain
Test plan
npm run typecheck+typecheck:apicleantests/resilience-source-failure.test.mts13/13 passingtests/resilience-dimension-scorers.test.mts63/63 passing (5 new integration cases)resilience-*.test.mts+.mjs) passingnpm run test:data4379/4379 passingnpm run lintexit 0get-resilience-scoreresponse showsgovernanceInstitutional.imputationClass === 'source-failure'for imputed countries while real-data countries keepnull.Post-Deploy Monitoring & Validation
failedDatasetsnon-empty runs. Vercel get-resilience-score RPC response bodies. Vercel logs for[Resilience] source-failure decorationinfo lines.failedDatasetsduring the next natural adapter failurecurl -s https://api.worldmonitor.app/worldmonitor/resilience/v1/get-resilience-score -d '{"countryCode":"XX"}' | jq '.domains[].dimensions[] | select(.imputationClass=="source-failure") | .id'source-failure. When one adapter fails, only imputed dimensions consuming that adapter are re-tagged.source-failure(indicates a reader bug). Revert.