feat(resilience): add shared statistical utilities#2656
Conversation
Port the resilience statistics helpers needed for country resilience scoring into server/_shared with a focused test suite. Refs koala73#2478 Validation: - npx tsx --test tests/resilience-stats.test.mts - npm run typecheck:api (fails on upstream main because server/__tests__/entitlement-check.test.ts imports vitest without the dependency in tsconfig.api scope)
|
@lspassos1 is attempting to deploy a commit to the Elie Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR introduces Confidence Score: 5/5Safe to merge — isolated utility module with no runtime wiring; all findings are P2 All four findings are P2 (style, edge-case robustness, naming); none describe a definite runtime defect on the changed path. The math is correct, the tests cover the documented contracts, and no existing code is modified. server/_shared/resilience-stats.ts — four minor items worth addressing before the downstream resilience handlers are wired in Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
H[history array] --> LEN{len >= 3?}
LEN -- No --> FLAT[Flat fallback\nlastValue +/- 10 pct\nprobability 50/50]
LEN -- Yes --> ES[exponentialSmoothing alpha]
LEN -- Yes --> LR[linearRegression slope]
ES --> BL[baseline = smoothed.last]
LR --> SL[slope]
BL & SL --> PROJ[projected = clampScore baseline + slope x day]
ES & H --> RMSE[modelError = RMSE history vs smoothed]
RMSE --> CI[expandedError = modelError x sqrt day]
PROJ --> CI
PROJ --> PROB[probabilityUp 0.5 +/- diff x 0.05 clamped 0.05 to 0.95]
CI & PROB --> OUT[ResilienceForecastResult]
FLAT --> OUT
Reviews (1): Last reviewed commit: "feat(resilience): add shared statistical..." | Re-trigger Greptile |
| confidenceIntervals: Array.from({ length: horizonDays }, () => ({ | ||
| lower: round(clampScore(lastValue * 0.9)), | ||
| upper: round(clampScore(lastValue * 1.1)), | ||
| level: 95, | ||
| })), |
There was a problem hiding this comment.
CI degenerates to
[0, 0] when lastValue is zero in the short-history fallback
The ±10% band collapses to [0, 0] whenever lastValue = 0 (or any value ≤ 1 where * 1.1 still rounds to 0). For a resilience score, a value of 0 is an extreme but valid state, and the resulting zero-width interval misrepresents uncertainty. A floor-based fallback (e.g. at least ±5 points) would be more informative.
| confidenceIntervals: Array.from({ length: horizonDays }, () => ({ | |
| lower: round(clampScore(lastValue * 0.9)), | |
| upper: round(clampScore(lastValue * 1.1)), | |
| level: 95, | |
| })), | |
| confidenceIntervals: Array.from({ length: horizonDays }, () => ({ | |
| lower: round(clampScore(lastValue - Math.max(lastValue * 0.1, 5))), | |
| upper: round(clampScore(lastValue + Math.max(lastValue * 0.1, 5))), | |
| level: 95, | |
| })), |
- cronbachAlpha: reject jagged rows (unequal row lengths → 0) - CUSUM: simplify double-normalized slack to constant 0.5 - nrcForecast: use modelError-scaled probability instead of raw delta - Add boundary test for nrcForecast at exactly 3 values - Add test for cronbachAlpha jagged row guard
…on guard detectChangepoints: - Track onset index (where accumulation began) instead of detection index - Reduce CUSUM slack from 0.5 to 0.25 to detect moderate step shifts - Require onset >= 2 to filter false positives from initial-regime artifacts - Tighten tests: assert exact count and onset range, add moderate shift case nrcForecast: - Return neutral empty result for non-positive horizonDays - Add test covering horizon=0 and negative values
- Rename cronbachAlpha param `items` → `observations` (matches row layout) - CI fallback uses ±max(5, 10%) so value=0 gets [0, 5] not [0, 0] - probabilityDown rounds from already-rounded probabilityUp (sum=1 invariant)
Summary
This ports the pure resilience statistics helpers needed for the country resilience work into
server/_shared, with a focused test file that locks the issue contract and the verified upstream formulas.Root cause
The resilience chain had no shared statistical utility module yet, so the later resilience handlers and scorers would have had to either duplicate math or embed it inside domain code. That would make the follow-on issues harder to validate and easier to regress.
Changes
server/_shared/resilience-stats.tswithcronbachAlpha,detectTrend,detectChangepoints,minMaxNormalize,exponentialSmoothing, andnrcForecastqadr110formulas for alpha, CUSUM changepoints, smoothing, linear-trend forecasting, and probability narrativestableinstead ofsidewaysand treating fewer than 3 samples as stabletests/resilience-stats.test.mtsfor the issue cases and the forecast/clamping contractValidation
npx tsx --test tests/resilience-stats.test.mtsnpm run typecheck:apiupstream/mainbecauseserver/__tests__/entitlement-check.test.tsimportsvitestwithout the dependency resolving insidetsconfig.api.jsonscopeRisk
Low. This is an isolated pure utility module plus a dedicated test file; no runtime wiring changed yet.
Refs #2478