Skip to content

feat(resilience): T1.6 full grid with imputation + freshness columns#2962

Merged
koala73 merged 3 commits into
mainfrom
fix/t1-6-full-grid-icon-freshness
Apr 11, 2026
Merged

feat(resilience): T1.6 full grid with imputation + freshness columns#2962
koala73 merged 3 commits into
mainfrom
fix/t1-6-full-grid-icon-freshness

Conversation

@koala73

@koala73 koala73 commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 T1.6 full grid slice. PR #2949 shipped the per-dimension confidence grid scaffold and explicitly deferred two columns because their proto fields had not landed yet. PR #2959 (imputationClass) and #2961 (freshness) added those fields. 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".

Depends on #2959 and #2961

This PR is stacked on top of PR #2961 (T1.5 propagation), which is itself stacked on PR #2959 (T1.7 schema pass). Base branch is fix/t1-5-freshness-propagation, not main. Do not merge until the stack lands in order: #2959, then #2961, then this.

What this PR commits

  • DimensionConfidence type gains imputationClass, staleness, and lastObservedAtMs (number | null, coerced from proto int64 string).
  • formatDimensionConfidence normalizes empty string, unknown values, and missing fields into typed nulls.
  • New helpers getImputationClassIcon, getImputationClassLabel, getStalenessLabel for compact glyph rendering and a11y labels.
  • ResilienceWidget.renderDimensionConfidenceCell renders two new columns:
    • Imputation-icon column between the coverage bar and the percentage (color-coded per class, hidden when null)
    • Freshness-dot column at the far right (green/yellow/red dot per staleness, hidden when null)
    • aria-label and title attributes describe each glyph for a11y.
  • CSS country-deep-dive.css: cell grid template expanded from 3 columns to 5 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 imputation classes and staleness levels so the gated-UI preview shows off the new columns.
  • Tests: formatDimensionConfidence normalization cases, glyph/label helpers, LOCKED_PREVIEW smoke test.

What is NOT in this PR

  • No scorer changes. source-failure icon/color is rendered but no scorer path emits that class yet; PR 4 of 5 wires the seed-meta failedDatasets consultation.
  • No removal of formatResilienceConfidence single-label fallback. Cleanup out of scope.
  • No new SVG assets. Text glyphs + CSS colors only.

Phase 1 dependency chain

Test plan

  • npm run typecheck + typecheck:api clean
  • tests/resilience-widget.test.mts passes new cases
  • Full resilience suite passes (237 tests)
  • test:data clean (4384 tests)
  • lint exit 0
  • Visual check via npm run dev (reviewer)
  • After merge, confirm country deep-dive widget shows 5-column rows with at least one country having a non-empty imputation icon and at least one with a non-green freshness dot

Post-Deploy Monitoring & Validation

  • What to monitor: country deep-dive widget render for a sample of countries with known imputed dimensions (small Pacific islands, low-coverage states).
  • Expected healthy signal: per-dimension grid renders 5 columns, imputation icons and freshness dots appear on the rows that have them, all title tooltips are non-empty and descriptive.
  • Failure signal / rollback trigger: grid renders blank columns or the row layout collapses, revert and reopen with the responsive CSS fix.
  • Validation window: manual visual pass post-deploy.
  • If no impact: this PR is frontend-only and does not touch any server contract or Redis write path. No ops alerting change required.

@vercel

vercel Bot commented Apr 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Apr 11, 2026 8:28pm

Request Review

@greptile-apps

greptile-apps Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR completes the Phase 1 T1.6 full grid by wiring the imputationClass (from #2959) and freshness (from #2961) fields into the per-dimension confidence grid, expanding each cell from 3 to 5 columns (label → bar → imputation icon → pct → freshness dot) and populating LOCKED_PREVIEW with a realistic mix of classes and staleness levels. The normalization layer, glyph/label helpers, CSS modifiers, and test suite are all well-structured; remaining findings are stale comments and a minor fixture semantic inconsistency.

Confidence Score: 5/5

Safe to merge once the stacked PRs (#2959, #2961) land; no runtime bugs found.

All findings are P2 (stale comments, fixture convention deviation). The normalization functions, type guards, h()-prop skipping of undefined, and CSS layout are all correct. Tests cover the new paths thoroughly.

resilience-widget-utils.ts (stale forward-looking comment block + LOCKED_PREVIEW fixture semantic mismatch); ResilienceWidget.ts (stale block comment around renderDimensionConfidenceGrid call)

Important Files Changed

Filename Overview
src/components/resilience-widget-utils.ts Adds DimensionImputationClass/DimensionStaleness types, normalization helpers, glyph/label maps, and updates formatDimensionConfidence to surface both new fields; LOCKED_PREVIEW fixture has two dimensions with imputationClass set despite having observed data (violates documented convention).
src/components/ResilienceWidget.ts Wires renderDimensionConfidenceCell to emit two new columns (imputation icon + freshness dot); contains a now-stale block comment that describes the imputation/freshness columns as future work when they are already implemented here.
src/styles/country-deep-dive.css Expands dimension-cell grid template from 3 to 5 columns; adds per-class color modifiers for imputation icons and freshness dots; responsive breakpoint tightens column sizing at 560px.
tests/resilience-widget.test.mts Adds normalization cases for imputationClass/staleness/lastObservedAtMs, glyph/label helper coverage, and a LOCKED_PREVIEW smoke test verifying at least one non-fresh staleness level is represented.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ResilienceScoreResponse] --> B[collectDimensionConfidences]
    B --> C[formatDimensionConfidence per dimension]
    C --> D1[normalizeImputationClass empty/unknown → null]
    C --> D2[normalizeStaleness empty/unknown → null]
    C --> D3[normalizeLastObservedAtMs string → number or null]
    D1 & D2 & D3 --> E[DimensionConfidence imputationClass / staleness / lastObservedAtMs]
    E --> F[renderDimensionConfidenceCell]
    F --> G1[label span]
    F --> G2[coverage bar]
    F --> G3[imputation icon getImputationClassIcon]
    F --> G4[pct span]
    F --> G5[freshness dot getStalenessLabel]
    G1 & G2 & G3 & G4 & G5 --> H[5-column CSS grid cell]
Loading

Comments Outside Diff (2)

  1. src/components/ResilienceWidget.ts, line 281-286 (link)

    P2 Stale forward-looking comment

    This comment was written for PR feat(resilience): per-dimension confidence grid in widget (T1.6) #2949 and now describes work that this very PR implements. The sentence "Imputation class icons (T1.7) and freshness badges (T1.5 full pass) land as additional columns once the schema exposes those fields through the response type" is no longer accurate — both columns are rendered in the renderDimensionConfidenceCell introduced below.

  2. src/components/resilience-widget-utils.ts, line 173-176 (link)

    P2 Stale forward-looking comment

    The comment still says "The downstream adds (imputation class icon from T1.7, freshness badge from T1.5) land as additional columns in later PRs once the schema exposes those fields through the response type." This PR is exactly those "later PRs" — the fields are now implemented. The comment should describe the current state rather than a future state that has already landed.

Reviews (1): Last reviewed commit: "feat(resilience): T1.6 full grid with im..." | Re-trigger Greptile

Comment on lines +46 to +48
{ id: 'cyberDigital', score: 72, coverage: 0.85, observedWeight: 0.85, imputedWeight: 0.15, imputationClass: '', freshness: { lastObservedAtMs: LOCKED_PREVIEW_FRESH_AT_MS, staleness: 'fresh' } },
{ id: 'logisticsSupply', score: 70, coverage: 0.8, observedWeight: 0.8, imputedWeight: 0.2, imputationClass: 'stable-absence', freshness: { lastObservedAtMs: LOCKED_PREVIEW_FRESH_AT_MS, staleness: 'fresh' } },
{ id: 'infrastructure', score: 65, coverage: 0.9, observedWeight: 0.9, imputedWeight: 0.1, imputationClass: '', freshness: { lastObservedAtMs: LOCKED_PREVIEW_FRESH_AT_MS, staleness: 'fresh' } },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fixture imputationClass set on non-fully-imputed dimensions

The DimensionConfidenceInput JSDoc states: "Empty string when the dimension has any observed data (i.e. the taxonomy only applies to fully-imputed dimensions)." Two dimensions in the fixture break this convention:

  • logisticsSupply (observedWeight: 0.8) → classifies as observed, yet carries imputationClass: 'stable-absence'
  • socialCohesion (observedWeight: 0.9) → same issue

For a blurred locked-preview this is harmless, but it makes the fixture a misleading reference for anyone studying the intended semantics. borderSecurity (observedWeight: 0, imputedWeight: 1) is the correct pattern for carrying an imputation class.

koala73 added a commit that referenced this pull request Apr 11, 2026
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
koala73 added 2 commits April 12, 2026 00:21
…ass)

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
…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.
@koala73
koala73 force-pushed the fix/t1-5-freshness-propagation branch from 5dff6be to 45114e1 Compare April 11, 2026 20:23
koala73 added a commit that referenced this pull request Apr 11, 2026
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
@koala73
koala73 force-pushed the fix/t1-6-full-grid-icon-freshness branch from cdc323b to 58b2540 Compare April 11, 2026 20:26
Base automatically changed from fix/t1-5-freshness-propagation to main April 11, 2026 20:27
@koala73
koala73 merged commit da6b80f into main Apr 11, 2026
10 checks passed
@koala73
koala73 deleted the fix/t1-6-full-grid-icon-freshness branch April 11, 2026 20:28
koala73 added a commit that referenced this pull request Apr 11, 2026
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
koala73 added a commit that referenced this pull request Apr 11, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant