docs(resilience): promote methodology to .mdx at CII parity (T1.3)#2945
Conversation
Why this PR? Ships Phase 1 T1.3 of the country-resilience reference-grade upgrade plan: promote the v1.0 methodology document from a scratch markdown file to a published MDX page at parity with the Country Instability Index (docs/country-instability-index.mdx). Before this PR there was no publicly documented resilience methodology page, only an internal markdown draft. The reference-grade upgrade plan's origin review explicitly called out methodological opacity as the biggest gap keeping the product below OECD/JRC standards, and committed to documenting every dimension, formula, goalpost, cadence, weight rationale, and imputation rule before any Phase 2 schema work begins. This PR is that documentation. What this PR commits: - git mv docs/methodology/resilience-index.md to docs/methodology/country-resilience-index.mdx so git history tracks the rename; edits land as modifications on the new path. - Adds MDX frontmatter (title + description) matching CII shape so the page renders correctly in the docs site and shows up in search. - Prepends a short v1.0 scope note that distinguishes the currently shipping behavior (this doc) from the planned v2.0 three-pillar rebuild (tracked in the reference-grade upgrade plan). Readers who land on the page know exactly what is live and what is coming. - Fixes an orphan table row (`| Food & Water | Mixed |`) that had drifted into the Overall Score section from an earlier edit and was breaking rendering around the score-formula block. - Rewrites the Imputation Taxonomy section to use the formal four-class naming from T1.7 (stable-absence, unmonitored, source-failure, not-applicable) and adds a mapping table from each concrete IMPUTATION / IMPUTE entry to its class. This is the public-facing surface of the T1.7 foundation PR. - Adds a Reproducibility Appendix listing every Redis key used by the scorer (score cache, ranking cache, history sorted set, intervals, seed-meta, static record, static index), the meaning of the `dataVersion` field, and a step-by-step procedure for reproducing any published country score by hand from a Redis snapshot. - Adds a Changelog section with a v1.0 entry that references the prerequisite PRs (#2821, #2847, #2858) and the Phase 1 work landing so far (T1.1 regression test, T1.4 dataVersion widget wire, T1.7 imputation taxonomy foundation), plus a v2.0 placeholder that summarizes the reference-grade upgrade plan for readers. - Adds editorial notes pointing at the Phase 1 T1.8 methodology doc linter that will enforce parity between this document and the indicator registry in _dimension-scorers.ts. What is deliberately NOT in this PR: - No v2.0 content presented as shipping. The three-pillar rebuild, recovery capacity pillar, penalized weighted mean aggregation, cross-index benchmark, and annual Reference Edition are all tracked in the reference-grade upgrade plan but not yet implemented. The changelog v2.0 section names them as planned work, not current behavior. - No methodology doc linter. That is T1.8 and ships separately so it can be reviewed as a tooling change. - No deletion of the old .md path. git mv preserves history; the old location renders a 404 which is expected. Prerequisite PRs verified merged: - #2821 (baseline / stress engine) - #2847 (formula revert + RSF direction fix) - #2858 (seed direct scoring) Testing: - Markdown-only change, no code. - npm run typecheck: clean - Pre-push hook (typecheck + build:full + version:check) passes. 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]>
Companion commit to 4e7f64a (which was a rename-only because git add after git mv re-staged the original file content). This commit adds the actual v1.0 content the T1.3 task requires: - MDX frontmatter (title + description) matching the Country Instability Index shape so the page renders in the docs site. - Short v1.0 scope note that distinguishes the currently shipping behavior (this doc) from the planned v2.0 three-pillar rebuild (tracked in the reference-grade upgrade plan). - Removes an orphan table row (`| Food & Water | Mixed |`) from the Overall Score section that had drifted from an earlier edit. - Rewrites the Imputation Taxonomy section to use the formal four-class naming from T1.7 (stable-absence, unmonitored, source-failure, not-applicable) with a mapping table from each concrete IMPUTATION / IMPUTE entry to its class. - Reproducibility Appendix listing every Redis key used by the scorer, the dataVersion semantics, and a step-by-step reproduction procedure. - Changelog section with v1.0 (prerequisite PRs + T1.1 / T1.4 / T1.7 landing) and v2.0 placeholder summarizing the reference-grade upgrade plan. - Editorial notes pointing at the T1.8 methodology doc linter that will enforce parity between this document and the indicator registry. Net change: 89 insertions, 13 deletions, file grows from 300 to 376 lines. Docs-only, no code, no runtime impact. 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]>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryPromotes the internal resilience-index draft to a published MDX page (
Confidence Score: 4/5Safe to merge after the broken link on line 8 is resolved; no runtime risk. One P1 finding: the hyperlink to docs/internal/country-resilience-upgrade-plan.md will produce a 404 on publish since the file does not exist. All other findings are P2 style suggestions. Resolving the broken link brings this to 5/5. docs/methodology/country-resilience-index.mdx — specifically line 8 (broken internal link). Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
RawData["Raw indicator data\n(IMF, World Bank, BIS, OFAC…)"]
Impute["Missing data?\nImputation Taxonomy\nstable-absence / unmonitored /\nsource-failure / not-applicable"]
Norm["Goalpost normalization\n0–100 per indicator"]
DimScore["Dimension score\nweighted blend of sub-metrics"]
Coverage["Coverage value\nweighted certainty"]
DomainScore["Domain score\ncoverage-weighted mean of dimensions"]
Overall["Overall score\ndomain-weighted sum"]
Classify["Resilience level\nHigh / Medium / Low"]
Flags["Supplementary flags\nlowConfidence · imputationShare\nbaselineScore · stressScore · trend"]
RawData --> Norm
RawData --> Impute
Impute --> Norm
Norm --> DimScore
Norm --> Coverage
DimScore --> DomainScore
Coverage --> DomainScore
DomainScore --> Overall
Overall --> Classify
Overall --> Flags
|
|
|
||
| The WorldMonitor Country Resilience Index (CRI) scores every country in the world on a 0-100 scale, combining long-run structural capacity with current operational stress to produce an actionable resilience metric. Rather than relying on static country risk ratings, the CRI updates every 6 hours from official and authoritative sources and exposes full provenance, coverage, and imputation context so analysts can see exactly *why* a score moved and how much of it is real data versus imputed. | ||
|
|
||
| This document is the v1.0 reference for the live product. A planned v2.0 upgrade (see the [reference-grade upgrade plan](../internal/country-resilience-upgrade-plan.md)) will rebuild the top-level shape into three pillars (structural readiness, live shock exposure, recovery capacity) with a partly non-compensatory aggregation, and ship an annual Reference Edition at citation quality. That work is tracked separately; everything documented below describes the **current shipping behavior**. |
There was a problem hiding this comment.
Broken link to non-existent file
../internal/country-resilience-upgrade-plan.md does not exist in the repository — a glob of docs/internal/ shows only docs/internal/youtube-desktop.md. Any reader who clicks this link on the published docs site will get a 404. For a page explicitly aimed at OECD/JRC methodological transparency, a broken link in the opening paragraph undermines that goal immediately.
The same non-existent path is referenced again in the Changelog at line 365 (prose only, not a hyperlink, so lower impact).
Either create the file before this PR merges, replace the link with a GitHub issue/PR URL, or change it to plain text until the internal doc exists.
…1.8) Why this PR? Ships Phase 1 T1.8 of the country-resilience reference-grade upgrade plan: add a test that fails loudly if the published methodology document drifts from the scorer's RESILIENCE_DIMENSION_ORDER. This is the discipline that keeps the methodology page trustworthy over time, a forever risk on composite indices per the OECD/JRC handbook. The linter runs on every test pass and checks four things: 1. Every dimension in RESILIENCE_DIMENSION_ORDER has an H4 subsection in the methodology document. 2. Every H4 subsection in the methodology maps to a real scorer dimension (no stale docs). 3. Every H4 subsection is either a mapped dimension or explicitly allowlisted (prevents typos and unwired new sections). 4. HEADING_TO_DIMENSION in the test file maps exactly onto RESILIENCE_DIMENSION_ORDER with no extras and no gaps. This makes the test file itself the single source of truth for how the doc labels map to scorer IDs. Location-agnostic: the linter looks for the methodology file at a short list of candidate paths and prefers the newer country-resilience-index.mdx once T1.3 lands on main. On the current origin/main it finds the older resilience-index.md and lints that. This keeps T1.8 independent of T1.3's merge order so the PRs can land in either sequence. What this PR commits: - New test file tests/resilience-methodology-lint.test.mts with 5 scenarios covering the four checks above plus a smoke test that the file locator works. - Hardcoded HEADING_TO_DIMENSION map (13 entries, one per scorer dimension) as the source of truth for the heading-to-ID mapping. Any future dimension add must update this map in lockstep with the scorer and the methodology doc, which is exactly the drift prevention we want. What is NOT in this PR: - No changes to the methodology document or the scorer. - No automated HTML comment markers in the mdx. The hardcoded map in the test file is simpler and produces the same drift-detection signal. - No integration with lint-staged or CI-specific gating. The linter runs as part of the standard test:data suite so it fires on every pre-push hook run. Prerequisite PRs verified merged: - #2821 (baseline / stress engine) - #2847 (formula revert + RSF direction fix) - #2858 (seed direct scoring) Related (not prerequisite) in-flight Phase 1 PRs 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 Testing: - npx tsx --test tests/resilience-methodology-lint.test.mts: 5/5 pass - npx tsx --test tests/resilience-*.test.mts tests/resilience-*.test.mjs: 176/176 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]>
Why this PR? Ships the foundation-only slice of Phase 1 T1.5 of the country- resilience reference-grade upgrade plan: a pure staleness classifier that maps a `lastObservedAt` timestamp and a source cadence to one of three staleness levels (fresh, aging, stale). This is the primitive that T1.6 (widget dimension confidence bar with freshness badge) and the later T1.5 scorer propagation pass both consume. Same pattern as the T1.7 foundation PR (#2944): define the type and the primitive in isolation with comprehensive tests, then land the consumer wiring in separate PRs so each unit is bounded and reviewable. What this PR commits: - New module `server/_shared/resilience-freshness.ts` (110 lines) exporting: - `ResilienceCadence` type union covering the 5 cadences the methodology document lists (realtime, daily, weekly, monthly, annual). - `StalenessLevel` type union: fresh / aging / stale. - `cadenceUnitMs(cadence)` helper returning a canonical duration per cadence: realtime = 1 hour, daily = 1 day, weekly = 7 days, monthly = 30 days, annual = 365 days. - `FRESH_MULTIPLIER` = 1.5 and `AGING_MULTIPLIER` = 3. A signal is fresh when age is strictly less than 1.5x its cadence unit, aging when strictly less than 3x, stale otherwise. - `classifyStaleness({ lastObservedAtMs, cadence, nowMs })` pure function returning `{ staleness, ageMs, ageInCadenceUnits }`. Null / undefined / NaN / future timestamps return stale with positive-infinity age. `nowMs` is accepted as a deterministic override for unit testing. - New test file `tests/resilience-freshness.test.mts` (170 lines, 10 tests covering cadence ordering, fresh/aging/stale classification across all 5 cadences, defensive handling of null/NaN/future timestamps, exact threshold boundaries, internal consistency, and classifier purity). What is deliberately NOT in this PR: - No changes to the 13 dimension scorers. Propagating `lastObservedAt` through each scorer and aggregating max age per dimension is the next slice of T1.5 and will consume this classifier as a pure import. - No schema changes (proto, OpenAPI, `ResilienceDimension` response type). The schema field `freshness: { lastObservedAt, staleness }` lands alongside the widget rendering in T1.6. - No widget rendering. T1.6 owns the per-dimension freshness badge UI and will call `classifyStaleness` at render time. Prerequisite PRs verified merged: - #2821 (baseline / stress engine) - #2847 (formula revert + RSF direction fix) - #2858 (seed direct scoring) Related in-flight Phase 1 PRs 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 Testing: - npx tsx --test tests/resilience-freshness.test.mts: 10/10 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 two review comments on PR #2945: 1. **P2 Medium: broken reference in the v1.0 scope note at line 8.** The intro linked to `../internal/country-resilience-upgrade-plan.md`, which resolves to `docs/internal/country-resilience-upgrade-plan.md`. That file lives on PR #2938 and is not in this PR's tree or on origin/main, so until #2938 merges the link would render as a 404 on the docs site. Dropped the markdown link and kept the text reference to "a separate reference-grade upgrade plan" so readers still know context exists elsewhere. The existing prose reference to the same file at line 365 is already inside backticks and is therefore a plain-text citation, not a link, so it stays. 2. **Additional suggestion: roads series shared between two dimensions.** `roadsPavedLogistics` (Logistics & Supply, weight 0.50) and `roadsPavedInfra` (Infrastructure, weight 0.35) both read from World Bank `IS.ROD.PAVE.ZS`. Added an explicit note under the Infrastructure dimension table clarifying that this is deliberate source reuse (Logistics uses it as a transit-viability proxy, Infrastructure uses it as a baseline-public-capital proxy) and pointing forward to the v2.0 plan's consolidation of shared upstream signals into a single indicator registry. No change to the indicator tables themselves; this is a clarifying paragraph. No content semantics change beyond the clarifying paragraph and the dead-link removal. Docs-only, no code. 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]>
…1.8) (#2946) Why this PR? Ships Phase 1 T1.8 of the country-resilience reference-grade upgrade plan: add a test that fails loudly if the published methodology document drifts from the scorer's RESILIENCE_DIMENSION_ORDER. This is the discipline that keeps the methodology page trustworthy over time, a forever risk on composite indices per the OECD/JRC handbook. The linter runs on every test pass and checks four things: 1. Every dimension in RESILIENCE_DIMENSION_ORDER has an H4 subsection in the methodology document. 2. Every H4 subsection in the methodology maps to a real scorer dimension (no stale docs). 3. Every H4 subsection is either a mapped dimension or explicitly allowlisted (prevents typos and unwired new sections). 4. HEADING_TO_DIMENSION in the test file maps exactly onto RESILIENCE_DIMENSION_ORDER with no extras and no gaps. This makes the test file itself the single source of truth for how the doc labels map to scorer IDs. Location-agnostic: the linter looks for the methodology file at a short list of candidate paths and prefers the newer country-resilience-index.mdx once T1.3 lands on main. On the current origin/main it finds the older resilience-index.md and lints that. This keeps T1.8 independent of T1.3's merge order so the PRs can land in either sequence. What this PR commits: - New test file tests/resilience-methodology-lint.test.mts with 5 scenarios covering the four checks above plus a smoke test that the file locator works. - Hardcoded HEADING_TO_DIMENSION map (13 entries, one per scorer dimension) as the source of truth for the heading-to-ID mapping. Any future dimension add must update this map in lockstep with the scorer and the methodology doc, which is exactly the drift prevention we want. What is NOT in this PR: - No changes to the methodology document or the scorer. - No automated HTML comment markers in the mdx. The hardcoded map in the test file is simpler and produces the same drift-detection signal. - No integration with lint-staged or CI-specific gating. The linter runs as part of the standard test:data suite so it fires on every pre-push hook run. Prerequisite PRs verified merged: - #2821 (baseline / stress engine) - #2847 (formula revert + RSF direction fix) - #2858 (seed direct scoring) Related (not prerequisite) in-flight Phase 1 PRs 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 Testing: - npx tsx --test tests/resilience-methodology-lint.test.mts: 5/5 pass - npx tsx --test tests/resilience-*.test.mts tests/resilience-*.test.mjs: 176/176 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]>
Why this PR? Ships the foundation-only slice of Phase 1 T1.5 of the country- resilience reference-grade upgrade plan: a pure staleness classifier that maps a `lastObservedAt` timestamp and a source cadence to one of three staleness levels (fresh, aging, stale). This is the primitive that T1.6 (widget dimension confidence bar with freshness badge) and the later T1.5 scorer propagation pass both consume. Same pattern as the T1.7 foundation PR (#2944): define the type and the primitive in isolation with comprehensive tests, then land the consumer wiring in separate PRs so each unit is bounded and reviewable. What this PR commits: - New module `server/_shared/resilience-freshness.ts` (110 lines) exporting: - `ResilienceCadence` type union covering the 5 cadences the methodology document lists (realtime, daily, weekly, monthly, annual). - `StalenessLevel` type union: fresh / aging / stale. - `cadenceUnitMs(cadence)` helper returning a canonical duration per cadence: realtime = 1 hour, daily = 1 day, weekly = 7 days, monthly = 30 days, annual = 365 days. - `FRESH_MULTIPLIER` = 1.5 and `AGING_MULTIPLIER` = 3. A signal is fresh when age is strictly less than 1.5x its cadence unit, aging when strictly less than 3x, stale otherwise. - `classifyStaleness({ lastObservedAtMs, cadence, nowMs })` pure function returning `{ staleness, ageMs, ageInCadenceUnits }`. Null / undefined / NaN / future timestamps return stale with positive-infinity age. `nowMs` is accepted as a deterministic override for unit testing. - New test file `tests/resilience-freshness.test.mts` (170 lines, 10 tests covering cadence ordering, fresh/aging/stale classification across all 5 cadences, defensive handling of null/NaN/future timestamps, exact threshold boundaries, internal consistency, and classifier purity). What is deliberately NOT in this PR: - No changes to the 13 dimension scorers. Propagating `lastObservedAt` through each scorer and aggregating max age per dimension is the next slice of T1.5 and will consume this classifier as a pure import. - No schema changes (proto, OpenAPI, `ResilienceDimension` response type). The schema field `freshness: { lastObservedAt, staleness }` lands alongside the widget rendering in T1.6. - No widget rendering. T1.6 owns the per-dimension freshness badge UI and will call `classifyStaleness` at render time. Prerequisite PRs verified merged: - #2821 (baseline / stress engine) - #2847 (formula revert + RSF direction fix) - #2858 (seed direct scoring) Related in-flight Phase 1 PRs 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 Testing: - npx tsx --test tests/resilience-freshness.test.mts: 10/10 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]>
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]>
* feat(resilience): per-dimension confidence grid in widget (T1.6)
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]>
* fix(resilience): ship CSS + preview data for T1.6 grid (PR #2949 review)
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]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Greptile P2 finding on PR #2959: cachedFetchJson and getCachedResilienceScores return pre-change payloads verbatim, so a resilience:score:v7 entry written before this PR lands lacks the imputationClass field. Downstream consumers that read dim.imputationClass get undefined for up to 6 hours until the cache TTL expires. Fix: add normalizeResilienceScoreResponse helper that defaults missing optional fields in place and apply it at both read sites. Defaults imputationClass to empty string, matching the proto3 default for the new imputation_class field. - ensureResilienceScoreCached applies the normalizer after cachedFetchJson returns. - getCachedResilienceScores applies it after each successful JSON.parse on the pipeline result. - Two new test cases: stale payload without imputationClass gets defaulted, present values are preserved. - Not bumping the cache key: stale-read defaults are safe, the key bump would invalidate every cached score for a 6-hour cold-start cycle. The normalizer is extensible when PR #2961 adds freshness to the same payload. P3 finding (broken docs reference) verified invalid: the proto comment points to docs/methodology/country-resilience-index.mdx, which IS the current file. The .md predecessor was renamed in PR #2945 (T1.3 methodology doc promotion to CII parity). No change needed to the comment.
… schema pass) (#2959) * feat(resilience): expose imputationClass on ResilienceDimension (T1.7 schema pass) Ships the Phase 1 T1.7 schema pass of the country-resilience reference grade upgrade plan. PR #2944 shipped the classifier table foundation (ImputationClass type, ImputationEntry interface, IMPUTATION/IMPUTE tagged with four semantic classes) and explicitly deferred the schema propagation. This PR lands that propagation so downstream consumers can distinguish "country is stable" from "country is unmonitored" from "upstream is down" from "structurally not applicable" on a per-dimension basis. What this PR commits - Proto: new imputation_class string field on ResilienceDimension (empty string = dimension has any observed data; otherwise one of stable-absence, unmonitored, source-failure, not-applicable). - Generated TS types: regenerated service_server.ts and service_client.ts via make generate. - Scorer: ResilienceDimensionScore carries ImputationClass | null. WeightedMetric carries an optional imputationClass that imputation paths populate. weightedBlend aggregates the dominant class by weight when the dimension is fully imputed, returns null otherwise. - All IMPUTE.* early-return paths propagate the class from the table (IMPUTE.bisEer, IMPUTE.wtoData, IMPUTE.ipcFood, IMPUTE.unhcrDisplacement). - Response builder: _shared.ts buildDimensionList passes the class through to the ResilienceDimension proto field. - Tests: weightedBlend aggregation semantics (5 cases), dimension-level propagation from IMPUTE tables, serialized response includes the field. What is deliberately NOT in this PR - No widget icon rendering (T1.6 full grid, PR 3 of 5) - No source-failure seed-meta consultation (PR 4 of 5) - No freshness field (T1.5 propagation, PR 2 of 5) - No cache key bump: the new field is empty-string default, existing cached responses continue to deserialize cleanly Verified - make generate clean - npm run typecheck + typecheck:api clean - tests/resilience-dimension-scorers.test.mts all passing (existing + new) - tests/resilience-*.test.mts + test:data suite passing (4361 tests) - npm run lint exits 0 * fix(resilience): normalize cached score responses on read (#2959 P2) Greptile P2 finding on PR #2959: cachedFetchJson and getCachedResilienceScores return pre-change payloads verbatim, so a resilience:score:v7 entry written before this PR lands lacks the imputationClass field. Downstream consumers that read dim.imputationClass get undefined for up to 6 hours until the cache TTL expires. Fix: add normalizeResilienceScoreResponse helper that defaults missing optional fields in place and apply it at both read sites. Defaults imputationClass to empty string, matching the proto3 default for the new imputation_class field. - ensureResilienceScoreCached applies the normalizer after cachedFetchJson returns. - getCachedResilienceScores applies it after each successful JSON.parse on the pipeline result. - Two new test cases: stale payload without imputationClass gets defaulted, present values are preserved. - Not bumping the cache key: stale-read defaults are safe, the key bump would invalidate every cached score for a 6-hour cold-start cycle. The normalizer is extensible when PR #2961 adds freshness to the same payload. P3 finding (broken docs reference) verified invalid: the proto comment points to docs/methodology/country-resilience-index.mdx, which IS the current file. The .md predecessor was renamed in PR #2945 (T1.3 methodology doc promotion to CII parity). No change needed to the comment. * fix(resilience): bump score cache key v7 to v8, drop normalizer (#2959 P2) Second fixup for the Greptile P2 finding on #2959. The previous fixup (40ea220) added normalizeResilienceScoreResponse to default missing imputationClass fields on cached payloads to empty string. The reviewer correctly pushed back: defaulting to empty string is the proto3 default for "dimension has observed data", which silently misreports pre-rollout imputed dimensions as observed until the 6h TTL expires. Correct fix: bump RESILIENCE_SCORE_CACHE_PREFIX from resilience:score:v7: to resilience:score:v8:. Invalidates every pre-change cache entry, so the next request per country repopulates with the correct imputationClass written by the scorer. Cost: a 6h warmup cycle where first-request-per-country recomputes the score, ~100ms per country across hundreds of requests. Also deletes the normalizeResilienceScoreResponse helper and its two call sites. It was misleading defense-in-depth that can hide future schema drift bugs. Future additive field additions should bump the key, not silently default fields. - server/worldmonitor/resilience/v1/_shared.ts: prefix v7 to v8, delete normalizer function and both call sites. - scripts/seed-resilience-scores.mjs, validate-resilience-correlation.mjs, validate-resilience-backtest.mjs: mirror constants bumped. - tests/resilience-scores-seed.test.mjs: pin literal v7 to v8. - tests/resilience-ranking.test.mts: 7 hardcoded cache keys bumped. - tests/resilience-handlers.test.mts: stray v7 cache key bumped. - tests/resilience-release-gate.test.mts: the two normalizer test cases from 40ea220 deleted along with the helper. - docs/methodology/country-resilience-index.mdx: Redis keys table updated from v7 to v8 to match the canonical constant. P3 (broken docs reference) confirmed invalid a second time. docs/methodology/country-resilience-index.mdx exists on origin/main AND on the PR branch with the same blob hash d2ab1eb. docs/methodology/resilience-index.md does not exist on either. No proto comment change.
Why this PR?
Ships Phase 1 T1.3 of the country-resilience reference-grade upgrade plan (PR #2938): promote the v1.0 methodology document from an internal markdown draft to a published MDX page at parity with the Country Instability Index.
Before this PR there was no publicly documented resilience methodology page, only the internal draft at
docs/methodology/resilience-index.md. The reference-grade upgrade plan's origin review explicitly called out methodological opacity as the biggest gap keeping the product below OECD/JRC standards, and committed to documenting every dimension, formula, goalpost, cadence, weight rationale, and imputation rule before any Phase 2 schema work begins. This PR is that documentation.What this PR commits
git mvdocs/methodology/resilience-index.md-->docs/methodology/country-resilience-index.mdxso history tracks the rename cleanly.stable-absence,unmonitored,source-failure,not-applicable) and adds a mapping table from each concreteIMPUTATION/IMPUTEentry to its class. This is the public-facing surface of PR feat(resilience): 4-class imputation taxonomy foundation (T1.7) #2944 (T1.7 foundation).dataVersionsemantics, and a step-by-step procedure for reproducing any published country score by hand from a Redis snapshot.dataVersionwidget wire, feat(resilience): 4-class imputation taxonomy foundation (T1.7) #2944 T1.7 imputation taxonomy foundation), plus a v2.0 placeholder summarizing the reference-grade upgrade plan._dimension-scorers.ts.Net effect: one file renamed and expanded from 300 to 376 lines, zero em dashes, zero code changes.
Why there are two commits on this branch
The first commit (
4e7f64aaa) is a pure rename, the second (332a39899) is the content delta. I rangit addaftergit mvand initially re-staged the original file content, so commit #1 landed as a rename-only. Commit #2 is the actual content. Per repo policy I do not amend commits once created, so this split is deliberate. Reviewers can squash-merge for a clean single-commit history or merge as-is; the net diff is identical.What is deliberately NOT in this PR
Prerequisite PRs (verified merged)
Testing
npm run typecheck: clean.Post-Deploy Monitoring & Validation
No additional operational monitoring required: docs-only change, no runtime impact. The new page renders at
/docs/methodology/country-resilience-index(or wherever the docs site mounts MDX content) and is immediately visible to external readers after deploy.Generated with Claude Opus 4.6 (1M context) via Claude Code