feat(resilience): expose imputationClass on ResilienceDimension (T1.7 schema pass)#2959
Conversation
… schema pass) Ships the Phase 1 T1.7 schema pass of the country-resilience reference grade upgrade plan. PR #2944 shipped the classifier table foundation (ImputationClass type, ImputationEntry interface, IMPUTATION/IMPUTE tagged with four semantic classes) and explicitly deferred the schema propagation. This PR lands that propagation so downstream consumers can distinguish "country is stable" from "country is unmonitored" from "upstream is down" from "structurally not applicable" on a per-dimension basis. What this PR commits - Proto: new imputation_class string field on ResilienceDimension (empty string = dimension has any observed data; otherwise one of stable-absence, unmonitored, source-failure, not-applicable). - Generated TS types: regenerated service_server.ts and service_client.ts via make generate. - Scorer: ResilienceDimensionScore carries ImputationClass | null. WeightedMetric carries an optional imputationClass that imputation paths populate. weightedBlend aggregates the dominant class by weight when the dimension is fully imputed, returns null otherwise. - All IMPUTE.* early-return paths propagate the class from the table (IMPUTE.bisEer, IMPUTE.wtoData, IMPUTE.ipcFood, IMPUTE.unhcrDisplacement). - Response builder: _shared.ts buildDimensionList passes the class through to the ResilienceDimension proto field. - Tests: weightedBlend aggregation semantics (5 cases), dimension-level propagation from IMPUTE tables, serialized response includes the field. What is deliberately NOT in this PR - No widget icon rendering (T1.6 full grid, PR 3 of 5) - No source-failure seed-meta consultation (PR 4 of 5) - No freshness field (T1.5 propagation, PR 2 of 5) - No cache key bump: the new field is empty-string default, existing cached responses continue to deserialize cleanly Verified - make generate clean - npm run typecheck + typecheck:api clean - tests/resilience-dimension-scorers.test.mts all passing (existing + new) - tests/resilience-*.test.mts + test:data suite passing (4361 tests) - npm run lint exits 0
|
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 lands the T1.7 schema pass: Confidence Score: 5/5Safe to merge — all findings are P2 documentation/comment inaccuracies with no impact on runtime behavior. The core implementation is correct: weightedBlend aggregation, tie-break ordering, four scorer propagation paths, and the null→'' mapping in buildDimensionList all behave as intended and are fully covered by the new test suites. The only issues are an incomplete empty-string semantics description and a stale comment, neither of which affects correctness of current code or any live consumer. server/worldmonitor/resilience/v1/_shared.ts — imputationClass comment should note the zero-coverage case. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Scorer"] --> B{Source loaded?}
B -- No --> C["score: null (no tag)"]
B -- Yes --> D{Country in source?}
D -- Yes --> E["Real metric\nimputed: false"]
D -- No --> F["Imputed metric\nimputed: true\nimputationClass: IMPUTE.X"]
C --> G["weightedBlend"]
E --> G
F --> G
G --> H{observedWeight > 0?}
H -- Yes --> I["imputationClass: null"]
H -- No --> J{imputedWeight > 0?}
J -- No --> K["imputationClass: null\n(no data at all)"]
J -- Yes --> L["Dominant class by weight\ntie-break: stable-absence\n> unmonitored > source-failure\n> not-applicable"]
L --> M["imputationClass: 'stable-absence' etc."]
I --> N["buildDimensionList: null ?? ''"]
K --> N
M --> N
N --> O["ResilienceDimension.imputationClass"]
|
| // T1.7 schema pass: empty string = dimension has any observed data. | ||
| imputationClass: scores[dimensionId].imputationClass ?? '', |
There was a problem hiding this comment.
Incomplete comment — empty string also applies to the zero-data case
The comment // T1.7 schema pass: empty string = dimension has any observed data. is only half the story. imputationClass is null (→ '') in three distinct situations from weightedBlend:
observedWeight > 0– at least one real data point (the described case)availableWeight === 0– no metrics have non-null scores at all (completely absent dimension)observedWeight === 0 && classWeights.size === 0– imputed but no class recorded (edge case)
A downstream consumer reading imputationClass === '' cannot conclude "has observed data" — they also need coverage > 0 to rule out the absent case. The same issue appears verbatim in the proto comment and OpenAPI description.
| // T1.7 schema pass: empty string = dimension has any observed data. | |
| imputationClass: scores[dimensionId].imputationClass ?? '', | |
| // T1.7 schema pass: empty string when the dimension has any observed data | |
| // OR when coverage=0 (no data at all — every metric returned null). A | |
| // non-empty value (e.g. "stable-absence") means the dimension is fully | |
| // imputed with no observed data. Consumers must check coverage > 0 to | |
| // distinguish "observed" from "absent". | |
| imputationClass: scores[dimensionId].imputationClass ?? '', |
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
Greptile P2 finding on PR #2959: cachedFetchJson and getCachedResilienceScores return pre-change payloads verbatim, so a resilience:score:v7 entry written before this PR lands lacks the imputationClass field. Downstream consumers that read dim.imputationClass get undefined for up to 6 hours until the cache TTL expires. Fix: add normalizeResilienceScoreResponse helper that defaults missing optional fields in place and apply it at both read sites. Defaults imputationClass to empty string, matching the proto3 default for the new imputation_class field. - ensureResilienceScoreCached applies the normalizer after cachedFetchJson returns. - getCachedResilienceScores applies it after each successful JSON.parse on the pipeline result. - Two new test cases: stale payload without imputationClass gets defaulted, present values are preserved. - Not bumping the cache key: stale-read defaults are safe, the key bump would invalidate every cached score for a 6-hour cold-start cycle. The normalizer is extensible when PR #2961 adds freshness to the same payload. P3 finding (broken docs reference) verified invalid: the proto comment points to docs/methodology/country-resilience-index.mdx, which IS the current file. The .md predecessor was renamed in PR #2945 (T1.3 methodology doc promotion to CII parity). No change needed to the comment.
|
Addressed P2 in commit 40ea220: added P3 (broken docs reference) is invalid: |
…P2) Second fixup for the Greptile P2 finding on #2959. The previous fixup (40ea220) added normalizeResilienceScoreResponse to default missing imputationClass fields on cached payloads to empty string. The reviewer correctly pushed back: defaulting to empty string is the proto3 default for "dimension has observed data", which silently misreports pre-rollout imputed dimensions as observed until the 6h TTL expires. Correct fix: bump RESILIENCE_SCORE_CACHE_PREFIX from resilience:score:v7: to resilience:score:v8:. Invalidates every pre-change cache entry, so the next request per country repopulates with the correct imputationClass written by the scorer. Cost: a 6h warmup cycle where first-request-per-country recomputes the score, ~100ms per country across hundreds of requests. Also deletes the normalizeResilienceScoreResponse helper and its two call sites. It was misleading defense-in-depth that can hide future schema drift bugs. Future additive field additions should bump the key, not silently default fields. - server/worldmonitor/resilience/v1/_shared.ts: prefix v7 to v8, delete normalizer function and both call sites. - scripts/seed-resilience-scores.mjs, validate-resilience-correlation.mjs, validate-resilience-backtest.mjs: mirror constants bumped. - tests/resilience-scores-seed.test.mjs: pin literal v7 to v8. - tests/resilience-ranking.test.mts: 7 hardcoded cache keys bumped. - tests/resilience-handlers.test.mts: stray v7 cache key bumped. - tests/resilience-release-gate.test.mts: the two normalizer test cases from 40ea220 deleted along with the helper. - docs/methodology/country-resilience-index.mdx: Redis keys table updated from v7 to v8 to match the canonical constant. P3 (broken docs reference) confirmed invalid a second time. docs/methodology/country-resilience-index.mdx exists on origin/main AND on the PR branch with the same blob hash d2ab1eb. docs/methodology/resilience-index.md does not exist on either. No proto comment change.
|
Replaced the normalize-on-read approach with a proper cache-key bump in commit 5b29040. What changed from 40ea220:
Cost of the bump: 6h warmup cycle where first-request-per-country recomputes the score, ~100ms per country across hundreds of requests. Acceptable. P3 (broken docs reference): confirmed invalid a second time. Verification: typecheck / typecheck:api / full resilience suite (214/214) / test:data (4361/4361) all clean. Pre-push hook passed. |
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
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
…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
…2964) * feat(resilience): T1.7 source-failure wiring + delete absence branch 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 * docs(resilience): update plan doc T1.7 PR number to #2964
Summary
Phase 1 T1.7 schema pass from the country-resilience reference-grade upgrade plan. PR #2944 shipped the classifier table foundation and explicitly deferred the schema propagation; this PR lands it so downstream consumers can distinguish the four semantic imputation classes on a per-dimension basis.
What this PR commits
imputation_classstring field onResilienceDimension(empty string when the dimension has any observed data; otherwise one ofstable-absence,unmonitored,source-failure,not-applicable).service_server.tsandservice_client.tsviamake generate.ResilienceDimensionScorecarriesImputationClass | null.WeightedMetriccarries an optionalimputationClassthat imputation paths populate.weightedBlendaggregates the dominant class by weight when the dimension is fully imputed, returnsnullotherwise.IMPUTE.bisEer,IMPUTE.wtoData,IMPUTE.ipcFood,IMPUTE.unhcrDisplacementall propagate their tagged class._shared.tsbuildDimensionListpasses the class through to the proto field.weightedBlendaggregation semantics (5 cases), dimension-level propagation fromIMPUTEtables, serialized response shape.What is NOT in this PR
not-applicableemission, reserved for future structural N/A cases (e.g. landlocked maritime exposure).resilience:score:v7deserialize cleanly.Phase 1 dependency chain
imputationClasson the protofreshnesson the protoTest plan
make generateclean, new field visible in regenerated typesnpm run typecheck+typecheck:apicleantests/resilience-dimension-scorers.test.mtspasses existing + new casestests/resilience-*.test.mtsfull suite passesnpm run test:datafull suite passes (4361 tests)npm run lintexits 0get-resilience-scoreRPC returnsimputationClasson dimensions for a known fully-imputed case (e.g. a small-population Pacific island wherewtoData/ipcFoodare absent)Post-Deploy Monitoring & Validation
No additional operational monitoring required: this PR only adds an optional string field to the response schema and propagates an existing in-memory class tag. Existing cached responses in
resilience:score:v7keep deserializing cleanly because the field defaults to empty string.