feat(resilience): per-dimension confidence grid in widget (T1.6)#2949
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryShips 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 Confidence Score: 5/5Safe 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
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]
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), |
There was a problem hiding this comment.
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:
| 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.
There was a problem hiding this comment.
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.
| * | ||
| * 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. |
There was a problem hiding this comment.
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:
| * | |
| * 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. |
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]>
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]>
8a89a6d to
1ff87a3
Compare
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 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
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,imputedWeightfields that are already on everyResilienceDimension, and leaves the two other columns to later PRs once the schema lands.What this PR commits
src/components/resilience-widget-utils.ts:DIMENSION_LABELSmap 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 existinggetResilienceDomainLabelpattern.DimensionConfidenceInput,DimensionCoverageStatus, andDimensionConfidencetypes.formatDimensionConfidence(input)pure function: returns{ id, label, coveragePct, status, absent }. Status is one ofobserved(80% or more of weight is real data),partial(mixed observed and imputed, less than 80% observed share),imputed(zero observed weight), orabsent(zero total weight). The 80% threshold mirrors the existinglowConfidencerule in_shared.tsapplied 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.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.renderScoreCardbetween the existing domain rows and the footer.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,collectDimensionConfidencesorder preservation, empty response.What is NOT in this PR
imputationClasson theResilienceDimensionresponse type (proto change). Tracked as a follow-up after the T1.7 schema pass.lastObservedAtand a staleness level on the response type (proto change). Tracked as a follow-up after the T1.5 full propagation pass.Prerequisite PRs verified merged
Related in-flight Phase 1 PRs from this session
dataVersionwidget wireTesting
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 passnpm run typecheck: cleanbuild:full+version:check)Post-Deploy Monitoring & Validation
n/aand coverage is clamped, so a malformedcoveragevalue cannot throw at render time.observedstatus for all 13 dimensions in the elite and strong tiers.resilience-widget__dimension-gridorresilience-widget__dimension-cell, or a visual regression where the grid renders blank or malformed. Rollback by reverting this PR; server-side behavior is unchanged.Generated with Claude Opus 4.6 (1M context) via Claude Code