test(resilience): T1.1 regression test for origin-doc ceiling claim#2941
Conversation
Why this PR? Ships the Phase 1 T1.1 deliverable from the country-resilience reference-grade upgrade plan: reproduce the origin-doc "Norway and US both hit 100" ceiling bug with a failing test before any fix lands. The plan explicitly committed to reproducing-before-fixing and to updating the origin doc's changelog if the symptom turned out to be misattributed. This PR documents the measured outcome and pins the current correct behavior. Investigation outcome: the claim does NOT reproduce. Measured scores under the current release-gate fixtures and the post-PR-#2847 domain-weighted-average formula: NO (elite tier) overallScore = 86.58, baseline 86.85, stress 84.36 US (strong tier) overallScore = 72.80, baseline 73.15, stress 70.58 Delta NO - US = 13.78 points Nothing pins at 100. The ordering elite > strong > stressed > fragile is preserved, and the domain-weighted average cannot reach 100 unless every dimension saturates (which does not happen for any fixture tier). The origin claim is misattributed or stale, probably predating PR #2847 which reverted the multiplicative baseline*(1-stressFactor) formula that had over-penalized all countries. What this PR commits: - A new regression test in tests/resilience-release-gate.test.mts asserting that NO and US are not pinned at 100, that NO > US, and that the delta is at least 3 points. The threshold leaves room for fixture tuning without over-fitting to the measured 13.78 delta. - A detailed comment block inside the test capturing the measured numbers, the conclusion, and the rationale so future regressions to a real ceiling bug are caught immediately and so the origin-doc changelog update (tracked separately) has a cite-able evidence base. The origin-doc changelog update is deliberately deferred to a trailing commit after PR #2938 (the reference-grade plan, which contains the origin doc) merges, to avoid a cross-branch conflict. Side finding, out of scope for this PR, tracked as a follow-up: the release-gate fixtures use qualityFor(profile) to derive all inputs from a single quality value per tier (elite/strong/stressed/ fragile), so every country within a tier produces identical scores. This means the release-gate suite cannot detect within-tier ordering issues. Not a scorer bug, a fixture-design limitation. Prerequisite PRs verified merged: - #2821 (baseline / stress engine) - #2847 (formula revert + RSF direction fix) - #2858 (seed direct scoring) Test suite: 172/172 resilience tests pass locally. 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]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds a single regression test ( Confidence Score: 5/5Test-only change with no runtime impact; safe to merge. All changes are confined to a single test file and follow existing patterns exactly. No production code, schema, or configuration is touched. The only open feedback is a P2 style suggestion about the delta threshold, which is a non-blocking design choice the author has already documented as intentional. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[T1.1 Test Start] --> B[installRedisFixtures]
B --> C[getResilienceScore NO + US in parallel]
C --> D{no.overallScore < 100?}
D -- fail --> E[❌ Norway pins at ceiling]
D -- pass --> F{us.overallScore < 100?}
F -- fail --> G[❌ US pins at ceiling]
F -- pass --> H{no.overallScore > us.overallScore?}
H -- fail --> I[❌ elite ≤ strong ordering broken]
H -- pass --> J{delta ≥ 3 points?}
J -- fail --> K[❌ near-tie: ranking degraded]
J -- pass --> L[✅ ceiling claim disproved, ordering preserved]
Reviews (1): Last reviewed commit: "test(resilience): T1.1 regression test f..." | Re-trigger Greptile |
| assert.ok( | ||
| no.overallScore - us.overallScore >= 3, | ||
| `Norway should lead the US by at least 3 points (NO=${no.overallScore}, US=${us.overallScore}, delta=${no.overallScore - us.overallScore})`, | ||
| ); |
There was a problem hiding this comment.
Consider raising the minimum-delta threshold
The measured delta at commit time is 13.78 points, but the assertion guards at only 3 points — less than a quarter of the actual gap. Any fixture change that silently erodes the margin to, say, 4 points would pass CI while representing a ~71% collapse in the tier separation signal. A threshold of roughly 8–10 points (≈60% of the measured delta) would give fixture tuning room while still providing a meaningful early-warning signal before the ordering becomes a near-tie.
| assert.ok( | |
| no.overallScore - us.overallScore >= 3, | |
| `Norway should lead the US by at least 3 points (NO=${no.overallScore}, US=${us.overallScore}, delta=${no.overallScore - us.overallScore})`, | |
| ); | |
| assert.ok( | |
| no.overallScore - us.overallScore >= 8, | |
| `Norway should lead the US by at least 8 points (NO=${no.overallScore}, US=${us.overallScore}, delta=${no.overallScore - us.overallScore})`, | |
| ); |
Not a blocker — the current threshold is explicitly documented as intentional — but worth revisiting if the fixture-tuning concern is considered low-risk.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fair catch, applied. Threshold is now >= 8 in ebee6c38d. The 60%-of-measured rule lands at roughly 8.3 against the observed 13.78, so 8 is the rounded floor. Added a comment explaining the choice and pointing at this thread so future reviewers do not re-discover the reasoning.
Test still passes under current fixtures: delta 13.78 >= 8.
…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 the P2 comment from Greptile on PR #2941: > "The measured delta at commit time is 13.78 points, but the > assertion guards at only 3 points, less than a quarter of the > actual gap. Any fixture change that silently erodes the margin > to, say, 4 points would pass CI while representing a ~71% > collapse in the tier separation signal. A threshold of roughly > 8 to 10 points (about 60% of the measured delta) would give > fixture tuning room while still providing a meaningful > early-warning signal before the ordering becomes a near-tie." Fair critique. The original commit-time reasoning ("3 leaves room for future fixture tuning without over-fitting to 13.78") optimized against over-fitting but allowed a semantic collapse from 13.78 to 4 to slip through CI. A threshold of 8 (about 60% of measured) catches that class of regression while still giving plenty of headroom for legitimate fixture tuning. The test still passes under current fixtures: actual delta is 13.78, assertion guards at >= 8. Also added a comment explaining the threshold choice and pointing at this review feedback, so the next reviewer does not need to re-discover the reasoning. No content change beyond the one threshold number, its error message, and the explanatory comment. 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]>
Appends an editor's note to the origin review preserving the original text as written and recording the Phase 1 T1.1 investigation outcome. The review states that "Norway and the US both hit 100 under current fixtures, which broke the intended ordering and exposed a ceiling effect at the top end of the ranking." The T1.1 regression test shipped on PR #2941 investigated this claim and did NOT reproduce it. Measured scores under the current release-gate fixtures and the post-PR-#2847 domain-weighted-average formula: Norway (elite tier): overallScore = 86.58, baseline 86.85, stress 84.36 US (strong tier): overallScore = 72.80, baseline 73.15, stress 70.58 Delta: NO minus US = 13.78 points Neither country approaches 100, the ordering is preserved, and the scorer cannot produce a hard 100 ceiling under any fixture tier. The specific Norway=US=100 illustration is retracted; the scorecard judgment and the six prescribed improvements remain valid. Also records the side finding from the investigation that the release-gate fixtures use one quality value per tier (elite, strong, stressed, fragile), so every country within a tier produces byte- identical scores. This is a fixture-design limitation, not a scorer bug, and is tracked as Phase 2 follow-up work. The original review text is preserved unchanged; the changelog is appended as an "Editor's Note" section at the end of the file so the historical record of what was originally filed stays auditable. The T1.1 regression test itself stays in the release-gate suite so a real ceiling bug, if ever introduced, is caught immediately by CI. Closes the one deliberate deferral noted in PR #2941's description (the "origin-doc changelog update" trailing commit that was waiting to land alongside the plan once the cross-branch conflict concern was resolved). Lands as a fourth commit on this PR so both the plan and the corrected origin doc ship together. 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]>
…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]>
Why this PR?
Ships Phase 1 T1.1 of the country-resilience reference-grade upgrade plan (PR #2938): a regression test that reproduces, or rather fails to reproduce, the origin-doc "Norway and US both hit 100" ceiling claim.
The plan explicitly committed to reproducing-before-fixing and to updating the origin doc's changelog if the symptom turned out to be misattributed. This PR documents the measured outcome and pins the current correct top-of-ranking behavior so any future regression to a real ceiling bug is caught by CI.
Investigation outcome
The origin-doc claim does not reproduce under the current release-gate fixtures and the post-PR-#2847 domain-weighted-average formula:
Delta NO minus US: 13.78 points. Neither approaches 100. The elite > strong > stressed > fragile ordering is preserved. The domain-weighted average cannot reach 100 unless every dimension saturates, which does not happen for any fixture tier.
The origin claim is misattributed or stale, probably predating PR #2847 which reverted the multiplicative
baseline * (1 - stressFactor)formula that had over-penalized all countries.What this PR commits
tests/resilience-release-gate.test.mtsasserting that NO and US are not pinned at 100, that NO > US, and that the delta is at least 3 points. The threshold leaves room for fixture tuning without over-fitting to the measured 13.78 delta.Follow-ups (not in this PR)
qualityFor(profile)derives every input from a single quality value per tier, so every country within a tier produces identical scores. This means the release-gate suite cannot detect within-tier ordering issues. Not a scorer bug, a fixture-design limitation. Worth a separate issue for Phase 2.Prerequisite PRs (verified merged)
Testing
npx tsx --test tests/resilience-*.test.mts tests/resilience-*.test.mjs: 172/172 passingnpm run typecheck: cleantypecheck+build:full+version:check; all passPost-Deploy Monitoring & Validation
No additional operational monitoring required: test-only change, no runtime impact. The new test runs in CI on every push.
Generated with Claude Opus 4.6 (1M context) via Claude Code