Skip to content

feat(resilience): add shared statistical utilities#2656

Merged
koala73 merged 5 commits into
koala73:mainfrom
lspassos1:feat/resilience-stats
Apr 4, 2026
Merged

feat(resilience): add shared statistical utilities#2656
koala73 merged 5 commits into
koala73:mainfrom
lspassos1:feat/resilience-stats

Conversation

@lspassos1

Copy link
Copy Markdown
Collaborator

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

  • add server/_shared/resilience-stats.ts with cronbachAlpha, detectTrend, detectChangepoints, minMaxNormalize, exponentialSmoothing, and nrcForecast
  • keep the verified qadr110 formulas for alpha, CUSUM changepoints, smoothing, linear-trend forecasting, and probability narrative
  • adapt the trend contract to WorldMonitor by returning stable instead of sideways and treating fewer than 3 samples as stable
  • add tests/resilience-stats.test.mts for the issue cases and the forecast/clamping contract

Validation

  • npx tsx --test tests/resilience-stats.test.mts
  • npm run typecheck:api
    • currently fails on upstream/main because server/__tests__/entitlement-check.test.ts imports vitest without the dependency resolving inside tsconfig.api.json scope

Risk
Low. This is an isolated pure utility module plus a dedicated test file; no runtime wiring changed yet.

Refs #2478

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)
@vercel

vercel Bot commented Apr 3, 2026

Copy link
Copy Markdown

@lspassos1 is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces server/_shared/resilience-stats.ts, a pure stateless utility module with six statistical helpers (cronbachAlpha, detectTrend, detectChangepoints, minMaxNormalize, exponentialSmoothing, nrcForecast) and a matching node:test suite. All algorithms are mathematically correct and no runtime wiring is changed. The only findings are four P2 style/robustness items: a redundant CUSUM intermediate variable that collapses to a constant, a misleading parameter name on cronbachAlpha, a degenerate [0, 0] confidence interval when the short-history fallback receives a zero score, and a rounding ordering in nrcForecast where probabilityUp + probabilityDown is not algebraically guaranteed to equal 1.0 for all inputs.

Confidence Score: 5/5

Safe 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

Filename Overview
server/_shared/resilience-stats.ts New pure-utility module exporting cronbachAlpha, detectTrend, detectChangepoints, minMaxNormalize, exponentialSmoothing, and nrcForecast; math is correct but four minor style/robustness issues found (redundant CUSUM slack variable, misleading parameter name, degenerate CI at value=0, and probabilityUp/Down sum not guaranteed to be exactly 1)
tests/resilience-stats.test.mts Dedicated test file using node:test covering all six exported functions with boundary cases; tests are well-structured and lock the documented contracts

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
Loading

Reviews (1): Last reviewed commit: "feat(resilience): add shared statistical..." | Re-trigger Greptile

Comment thread server/_shared/resilience-stats.ts Outdated
Comment thread server/_shared/resilience-stats.ts Outdated
Comment thread server/_shared/resilience-stats.ts Outdated
Comment on lines +172 to +176
confidenceIntervals: Array.from({ length: horizonDays }, () => ({
lower: round(clampScore(lastValue * 0.9)),
upper: round(clampScore(lastValue * 1.1)),
level: 95,
})),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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,
})),

Comment thread server/_shared/resilience-stats.ts Outdated
@lspassos1
lspassos1 requested a review from koala73 April 3, 2026 20:17
koala73 added 4 commits April 4, 2026 07:39
- 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)
@koala73
koala73 merged commit 152ac7e into koala73:main Apr 4, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants