Skip to content

feat(resilience): per-dimension confidence grid in widget (T1.6)#2949

Merged
koala73 merged 2 commits into
mainfrom
feat/resilience-dimension-confidence
Apr 11, 2026
Merged

feat(resilience): per-dimension confidence grid in widget (T1.6)#2949
koala73 merged 2 commits into
mainfrom
feat/resilience-dimension-confidence

Conversation

@koala73

@koala73 koala73 commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Why this PR?

Ships Phase 1 T1.6 of the country-resilience reference-grade upgrade plan (PR #2938): a compact per-dimension coverage grid below the 5-domain rows in the resilience widget so analysts can see per-dimension data provenance without opening the deep-dive panel.

Narrowed scope: the plan's full T1.6 description also adds an imputation class icon and a freshness badge per dimension, but both of those require proto schema additions that have not landed yet (T1.7 foundation in #2944 and T1.5 foundation in #2947 introduced the types and the classifier, but neither exposes the fields through the response schema). This PR ships the coverage column immediately using only the existing coverage, observedWeight, imputedWeight fields that are already on every ResilienceDimension, and leaves the two other columns to later PRs once the schema lands.

What this PR commits

  • New utils in src/components/resilience-widget-utils.ts:
    • DIMENSION_LABELS map with short display labels for each of the 13 scorer dimensions (Macro, Currency, Trade, Cyber, Logistics, Infra, Energy, Gov, Social, Border, Info, Health, Food).
    • getResilienceDimensionLabel(dimensionId), matching the existing getResilienceDomainLabel pattern.
    • DimensionConfidenceInput, DimensionCoverageStatus, and DimensionConfidence types.
    • formatDimensionConfidence(input) pure function: returns { id, label, coveragePct, status, absent }. Status is one of observed (80% or more of weight is real data), partial (mixed observed and imputed, less than 80% observed share), imputed (zero observed weight), or absent (zero total weight). The 80% threshold mirrors the existing lowConfidence rule in _shared.ts applied per dimension so one well-covered dimension is not obscured by the domain's worst case.
    • collectDimensionConfidences(domains) helper that walks every domain and every dimension in scorer order so the widget renders a stable grid.
  • New render methods in src/components/ResilienceWidget.ts:
    • renderDimensionConfidenceGrid(data) produces the container.
    • renderDimensionConfidenceCell(dim) produces one row per dimension with label, coverage bar, and percentage. Status modifier is on the cell className (--observed, --partial, --imputed, --absent) so CSS can style each bucket differently.
    • Wired into renderScoreCard between the existing domain rows and the footer.
  • 8 new tests in tests/resilience-widget.test.mts: all 13 dimension labels plus the unknown-ID fallback, observed-heavy, partial, all-imputed, zero-weight absent, clamping for out-of-range and NaN inputs, collectDimensionConfidences order preservation, empty response.

What is NOT in this PR

  • No imputation class icon per dimension. Requires exposing imputationClass on the ResilienceDimension response type (proto change). Tracked as a follow-up after the T1.7 schema pass.
  • No freshness badge per dimension. Requires exposing lastObservedAt and a staleness level on the response type (proto change). Tracked as a follow-up after the T1.5 full propagation pass.
  • No CSS changes. The new cell modifier classes are scaffolded for styling but the actual stylesheet edits will land in the CSS pass that picks up the full three-column row once the icon and badge columns exist.

Prerequisite PRs verified merged

Related in-flight Phase 1 PRs from this session

Testing

  • npx tsx --test tests/resilience-widget.test.mts: 14/14 pass (6 existing + 8 new dimension-confidence tests)
  • npx tsx --test tests/resilience-*.test.mts tests/resilience-*.test.mjs: 179/179 pass
  • npm run typecheck: clean
  • Pre-push hook passes (typecheck + build:full + version:check)

Post-Deploy Monitoring & Validation

  • What to monitor: watch for widget render failures in Sentry after deploy. The new cells are rendered defensively: absent dimensions show n/a and coverage is clamped, so a malformed coverage value cannot throw at render time.
  • Expected healthy signal: the resilience widget shows a small 13-cell grid between the domain bars and the footer, one cell per scorer dimension, with coverage percentages and status-modifier classes applied. Current fixtures on the release-gate suite produce observed status for all 13 dimensions in the elite and strong tiers.
  • Failure signal / rollback trigger: Sentry spike tagged to resilience-widget__dimension-grid or resilience-widget__dimension-cell, or a visual regression where the grid renders blank or malformed. Rollback by reverting this PR; server-side behavior is unchanged.
  • Validation window & owner: first 24h after merge, widget-owning team.

Compound Engineering v2.49.0

Generated with Claude Opus 4.6 (1M context) via Claude Code

@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 3:58pm

Request Review

@greptile-apps

greptile-apps Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Ships T1.6 Phase 1 of the resilience widget upgrade: a compact 13-cell per-dimension coverage grid rendered between the domain rows and footer, using only the existing coverage, observedWeight, and imputedWeight fields already present on every ResilienceDimension. New utility functions classify each dimension into observed / partial / imputed / absent buckets with correct NaN and out-of-range guards, and 8 new tests cover all branches.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/future-proofing concerns with no current runtime impact.

All new logic is well-guarded (NaN, clamp, zero-weight), comprehensively tested (8 new tests, 14/14 pass), and additive-only with no proto or schema changes. The two findings are P2: the empty-grid container is harmless while CSS is absent, and the misleading 'mirrors' comment is a doc nit. No P0/P1 issues found.

src/components/ResilienceWidget.ts — the unconditional grid render (empty container in locked preview) should be addressed before the CSS pass lands.

Important Files Changed

Filename Overview
src/components/resilience-widget-utils.ts Adds DIMENSION_LABELS map, DimensionConfidence types, formatDimensionConfidence, and collectDimensionConfidences; logic is sound with correct NaN/clamping guards.
src/components/ResilienceWidget.ts Wires dimension confidence grid into renderScoreCard unconditionally; LOCKED_PREVIEW has empty dimensions, so the grid always renders as an empty container in the gated view.
tests/resilience-widget.test.mts 8 new tests cover all 4 status buckets, NaN/clamping, order preservation, and empty inputs; comprehensive and well-structured.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ResilienceScoreResponse] --> B[collectDimensionConfidences]
    B --> C{For each domain}
    C --> D{For each dimension}
    D --> E[formatDimensionConfidence]
    E --> F{total weight <= 0?}
    F -- yes --> G[status: absent\ncoveragePct: 0\nabsent: true]
    F -- no --> H{observed === 0?}
    H -- yes --> I[status: imputed]
    H -- no --> J{observedShare >= 0.8?}
    J -- yes --> K[status: observed]
    J -- no --> L[status: partial]
    G --> M[DimensionConfidence]
    I --> M
    K --> M
    L --> M
    M --> N[renderDimensionConfidenceGrid]
    N --> O[renderDimensionConfidenceCell per dim]
    O --> P[label + bar-track + pct span]
Loading

Reviews (1): Last reviewed commit: "feat(resilience): per-dimension confiden..." | Re-trigger Greptile

// this ships without proto changes. 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.
this.renderDimensionConfidenceGrid(data),

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 Empty grid container rendered in locked preview

LOCKED_PREVIEW sets dimensions: [] on every domain, so collectDimensionConfidences returns an empty array and renderDimensionConfidenceGrid always emits <div class="resilience-widget__dimension-grid"> with no children in the gated/preview path. Currently harmless (no CSS yet), but once the CSS pass lands the container will carry any padding/border/min-height defined for .resilience-widget__dimension-grid, creating an invisible-but-spaced gap in the locked preview widget.

Consider guarding the render so the container is only emitted when there is at least one dimension:

Suggested change
this.renderDimensionConfidenceGrid(data),
dimensions.length > 0 ? this.renderDimensionConfidenceGrid(data) : null,

Or inside renderDimensionConfidenceGrid:

private renderDimensionConfidenceGrid(data: ResilienceScoreResponse): HTMLElement | null {
  const dimensions = collectDimensionConfidences(data.domains);
  if (dimensions.length === 0) return null;
  return h(
    'div',
    { className: 'resilience-widget__dimension-grid', title: '…' },
    ...dimensions.map((dim) => this.renderDimensionConfidenceCell(dim)),
  );
}

This also avoids surprising the CSS author who will write the grid styles later.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Applied in 8a89a6d. Both issues addressed in one pass.

1. CSS. Added .resilience-widget__dimension-grid (2-column on desktop, 1-column under 560px), .__dimension-cell with a 72px label and 28px pct column, .__dimension-bar-track / .__dimension-bar-fill, and four status modifiers (--observed #84cc16, --partial #eab308, --imputed #f97316, --absent text-faint) that reuse the existing resilience visual-level palette so the grid stays in the same chromatic family as the domain bars. Inserted between .__domains and .__footer in src/styles/country-deep-dive.css so ordering stays obvious, plus a mobile breakpoint rule so the grid collapses to one column under 560px.

2. LOCKED_PREVIEW preview data. Populated the gated fixture with representative 13-dimension entries (id, score, coverage, observedWeight, imputedWeight) so non-entitled users see a blurred grid that matches the shape of a real card. The preview stays non-interactive via the existing .resilience-widget__preview CSS class; exact values do not need to match any real country.

3. Moved LOCKED_PREVIEW to resilience-widget-utils.ts so a new regression test can import it without dragging in the full ResilienceWidget class (the class indirectly depends on import.meta.env.DEV via proxy.ts, which breaks plain node test runners). New test: LOCKED_PREVIEW populates all 13 dimensions for the gated preview asserts 13 entries, no raw IDs leaking through, no absent cells. 15/15 widget tests pass, 180/180 full resilience surface passes, typecheck clean.

Comment on lines +131 to +136
*
* The 80% threshold mirrors the existing `lowConfidence` rule in
* `_shared.ts` where imputation share above 40% (i.e. below 60% observed)
* flips the widget-wide low-confidence flag. The per-dimension threshold
* is stricter because a single well-covered dimension should not be
* obscured by the domain's worst case.

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 Comment says "mirrors" but the thresholds differ

The comment reads "The 80% threshold mirrors the existing lowConfidence rule … where imputation share above 40% (i.e. below 60% observed) flips the widget-wide low-confidence flag." The thresholds are 80% here vs 60% there — the same sentence then clarifies the per-dimension rule is intentionally stricter, so the word "mirrors" contradicts the intent. A small clarification avoids confusion:

Suggested change
*
* The 80% threshold mirrors the existing `lowConfidence` rule in
* `_shared.ts` where imputation share above 40% (i.e. below 60% observed)
* flips the widget-wide low-confidence flag. The per-dimension threshold
* is stricter because a single well-covered dimension should not be
* obscured by the domain's worst case.
* The 80% threshold is intentionally stricter than the widget-wide
* `lowConfidence` rule in `_shared.ts`, which flips at 60% observed
* share (imputation share > 40%). A single well-covered dimension
* should not be obscured by the domain's worst case.

koala73 added a commit that referenced this pull request Apr 11, 2026
Addresses the P2 REQUEST_CHANGES review on PR #2949:

> "New confidence-grid DOM is added without matching stylesheet
>  support. The widget now renders resilience-widget__dimension-grid
>  and resilience-widget__dimension-cell, but the stylesheet only
>  covers the existing domain rows and footer. That means the new
>  section will render as a tall unstyled stack instead of the
>  compact grid the PR describes. The locked preview path also stays
>  effectively empty because LOCKED_PREVIEW still has empty dimension
>  arrays, so gated users get a blank gap instead of a representative
>  preview."

Two changes in one pass:

1. **CSS for the dimension grid.** Added .resilience-widget__dimension-grid
   (2-column grid on desktop, 1-column under 560px), .__dimension-cell
   (72px label + flex bar + 28px pct), .__dimension-bar-track and
   .__dimension-bar-fill, .__dimension-label, .__dimension-pct, plus
   the four status modifiers (--observed, --partial, --imputed,
   --absent) which tint the bar fill with the existing resilience
   visual-level palette (#84cc16 observed, #eab308 partial, #f97316
   imputed, text-faint absent) so the grid stays in the same
   chromatic family as the domain bars. Added a mobile breakpoint
   rule so the grid collapses to one column on narrow widths.
   Inserted between the existing .__domains and .__footer rules at
   src/styles/country-deep-dive.css so ordering stays obvious.

2. **Populated LOCKED_PREVIEW with representative dimension data.**
   Every domain in the locked preview now carries real-looking
   dimension entries (id, score, coverage, observedWeight,
   imputedWeight) so non-entitled users see a blurred grid that
   matches the shape of a real card, not a blank gap between the
   domain bars and the footer. The exact values do not need to match
   any real country (the preview is blurred + non-interactive via
   the .resilience-widget__preview CSS rule), they just need to fill
   all 13 dimensions with plausible coverage values.

Also moved LOCKED_PREVIEW out of ResilienceWidget.ts and into
resilience-widget-utils.ts so the new regression test (see below)
can import it without dragging in the full ResilienceWidget class
transitive graph. The class indirectly depends on `import.meta.env.DEV`
via proxy.ts, which breaks plain node test runners. The utils file is
already dependency-free, so putting the fixture there is consistent
with the existing split between pure helpers and runtime widget code.

New regression test in tests/resilience-widget.test.mts:
`LOCKED_PREVIEW populates all 13 dimensions for the gated preview`
asserts that collectDimensionConfidences(LOCKED_PREVIEW.domains)
returns exactly 13 entries, every cell resolves to a short display
label (no raw IDs leaking through), and no cell is `absent`. If a
future edit accidentally drops a dimension from the preview, this
test fails loudly instead of producing a silent blank gap for gated
users.

Testing:
- npx tsx --test tests/resilience-widget.test.mts: 15/15 pass
  (14 existing + 1 new LOCKED_PREVIEW regression)
- npx tsx --test tests/resilience-*.test.mts tests/resilience-*.test.mjs:
  180/180 pass
- npm run typecheck: clean

Addresses the reviewer's requested changes directly; no DOM changes,
no new helpers, no scope expansion beyond the CSS + preview-data
pass.

Generated with Claude Opus 4.6 (1M context) via Claude Code
+ Compound Engineering v2.49.0

Co-Authored-By: Claude Opus 4.6 <[email protected]>
koala73 and others added 2 commits April 11, 2026 19:53
Why this PR?

Ships Phase 1 T1.6 of the country-resilience reference-grade upgrade
plan: a compact per-dimension coverage grid below the 5-domain rows
in the resilience widget so analysts can see per-dimension data
provenance without opening the deep-dive panel.

This is a scope-narrowed slice of T1.6. The plan's full description
adds an imputation class icon and a freshness badge per dimension,
but both of those require proto schema additions that have not
landed yet (T1.7 foundation and T1.5 foundation introduced the types
and classifier, but neither exposes the fields through the response
schema). This PR ships the coverage column immediately using the
existing `coverage`, `observedWeight`, `imputedWeight` fields that
are already on every ResilienceDimension, and leaves two follow-up
columns (imputation class icon, freshness badge) to later PRs once
the schema lands.

What this PR commits:

- New utils in `src/components/resilience-widget-utils.ts`:
    - `DIMENSION_LABELS` map with short display labels for each of
      the 13 scorer dimensions (`Macro`, `Currency`, `Trade`, `Cyber`,
      `Logistics`, `Infra`, `Energy`, `Gov`, `Social`, `Border`,
      `Info`, `Health`, `Food`).
    - `getResilienceDimensionLabel(dimensionId)` helper, matching
      the existing `getResilienceDomainLabel` pattern.
    - `DimensionConfidenceInput`, `DimensionCoverageStatus`, and
      `DimensionConfidence` types for the confidence classifier.
    - `formatDimensionConfidence(input)` pure function: returns
      `{ id, label, coveragePct, status, absent }` where status is
      one of `observed`, `partial`, `imputed`, `absent`. The 80%
      observed-share threshold for `observed` vs `partial` matches
      the existing `lowConfidence` rule in `_shared.ts` (where a 40%
      imputation share trips the widget-wide flag), applied per
      dimension so one well-covered dimension is not obscured by the
      domain's worst case.
    - `collectDimensionConfidences(domains)` helper that walks every
      domain and every dimension in scorer order so the widget
      renders a stable grid.
- New render methods in `src/components/ResilienceWidget.ts`:
    - `renderDimensionConfidenceGrid(data)` produces the container.
    - `renderDimensionConfidenceCell(dim)` produces one row per
      dimension with label, coverage bar, and percentage. Status
      enum is on the cell className so CSS can style observed,
      partial, imputed, and absent cells differently.
    - Wired into `renderScoreCard` between the existing domain rows
      and the footer, so the layout is domains, dimension grid,
      footer.
- 8 new tests in `tests/resilience-widget.test.mts` covering:
    - All 13 dimension labels plus the unknown-ID fallback.
    - Observed-heavy classification (observed).
    - Mixed observed and imputed classification (partial).
    - All-imputed classification (imputed).
    - Zero-weight absent classification (absent, `coveragePct=0`,
      `absent: true`).
    - Clamping for out-of-range coverage (above 1, negative) and
      NaN-safe fallback to zero weight and absent status.
    - `collectDimensionConfidences` preserves scorer order across
      domains and returns empty lists for empty responses.

What is deliberately NOT in this PR:

- No imputation class icon per dimension. That requires exposing
  `imputationClass` on the `ResilienceDimension` response type
  (proto change). Tracked as a follow-up after the T1.7 schema pass.
- No freshness badge per dimension. That requires exposing
  `lastObservedAt` and a staleness level on the response type (proto
  change). Tracked as a follow-up after the T1.5 full propagation pass.
- No CSS changes. The new cell classes are scaffolded for styling
  (`--observed`, `--partial`, `--imputed`, `--absent` modifiers) but
  the actual stylesheet edits will be folded into the CSS pass that
  picks up the full three-column dimension row once the icon and
  badge columns land.

Prerequisite PRs verified merged:
- #2821 (baseline / stress engine)
- #2847 (formula revert + RSF direction fix)
- #2858 (seed direct scoring)

Related in-flight Phase 1 PRs from this session:
- #2941 T1.1 regression test
- #2943 T1.4 dataVersion widget wire
- #2944 T1.7 imputation taxonomy foundation
- #2945 T1.3 methodology mdx promotion
- #2946 T1.8 methodology doc linter
- #2947 T1.5 staleness classifier foundation

Testing:
- npx tsx --test tests/resilience-widget.test.mts: 14/14 pass
  (6 existing + 8 new dimension-confidence tests)
- npx tsx --test tests/resilience-*.test.mts tests/resilience-*.test.mjs:
  179/179 pass
- npm run typecheck: clean

Generated with Claude Opus 4.6 (1M context) via Claude Code
+ Compound Engineering v2.49.0

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Addresses the P2 REQUEST_CHANGES review on PR #2949:

> "New confidence-grid DOM is added without matching stylesheet
>  support. The widget now renders resilience-widget__dimension-grid
>  and resilience-widget__dimension-cell, but the stylesheet only
>  covers the existing domain rows and footer. That means the new
>  section will render as a tall unstyled stack instead of the
>  compact grid the PR describes. The locked preview path also stays
>  effectively empty because LOCKED_PREVIEW still has empty dimension
>  arrays, so gated users get a blank gap instead of a representative
>  preview."

Two changes in one pass:

1. **CSS for the dimension grid.** Added .resilience-widget__dimension-grid
   (2-column grid on desktop, 1-column under 560px), .__dimension-cell
   (72px label + flex bar + 28px pct), .__dimension-bar-track and
   .__dimension-bar-fill, .__dimension-label, .__dimension-pct, plus
   the four status modifiers (--observed, --partial, --imputed,
   --absent) which tint the bar fill with the existing resilience
   visual-level palette (#84cc16 observed, #eab308 partial, #f97316
   imputed, text-faint absent) so the grid stays in the same
   chromatic family as the domain bars. Added a mobile breakpoint
   rule so the grid collapses to one column on narrow widths.
   Inserted between the existing .__domains and .__footer rules at
   src/styles/country-deep-dive.css so ordering stays obvious.

2. **Populated LOCKED_PREVIEW with representative dimension data.**
   Every domain in the locked preview now carries real-looking
   dimension entries (id, score, coverage, observedWeight,
   imputedWeight) so non-entitled users see a blurred grid that
   matches the shape of a real card, not a blank gap between the
   domain bars and the footer. The exact values do not need to match
   any real country (the preview is blurred + non-interactive via
   the .resilience-widget__preview CSS rule), they just need to fill
   all 13 dimensions with plausible coverage values.

Also moved LOCKED_PREVIEW out of ResilienceWidget.ts and into
resilience-widget-utils.ts so the new regression test (see below)
can import it without dragging in the full ResilienceWidget class
transitive graph. The class indirectly depends on `import.meta.env.DEV`
via proxy.ts, which breaks plain node test runners. The utils file is
already dependency-free, so putting the fixture there is consistent
with the existing split between pure helpers and runtime widget code.

New regression test in tests/resilience-widget.test.mts:
`LOCKED_PREVIEW populates all 13 dimensions for the gated preview`
asserts that collectDimensionConfidences(LOCKED_PREVIEW.domains)
returns exactly 13 entries, every cell resolves to a short display
label (no raw IDs leaking through), and no cell is `absent`. If a
future edit accidentally drops a dimension from the preview, this
test fails loudly instead of producing a silent blank gap for gated
users.

Testing:
- npx tsx --test tests/resilience-widget.test.mts: 15/15 pass
  (14 existing + 1 new LOCKED_PREVIEW regression)
- npx tsx --test tests/resilience-*.test.mts tests/resilience-*.test.mjs:
  180/180 pass
- npm run typecheck: clean

Addresses the reviewer's requested changes directly; no DOM changes,
no new helpers, no scope expansion beyond the CSS + preview-data
pass.

Generated with Claude Opus 4.6 (1M context) via Claude Code
+ Compound Engineering v2.49.0

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@koala73
koala73 force-pushed the feat/resilience-dimension-confidence branch from 8a89a6d to 1ff87a3 Compare April 11, 2026 15:56
@koala73
koala73 merged commit 36ba880 into main Apr 11, 2026
10 checks passed
@koala73
koala73 deleted the feat/resilience-dimension-confidence branch April 11, 2026 15:59
koala73 added a commit that referenced this pull request Apr 11, 2026
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 added a commit that referenced this pull request Apr 11, 2026
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 added a commit that referenced this pull request Apr 11, 2026
…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
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