* docs(resilience): PR 5.3 — foodWater scorer audit (construct-deterministic GCC identity)
PR 5.3 of cohort-audit plan 2026-04-24-002. Stacked on PR 5.2 (#3373)
so the known-limitations.md section append is additive. Read-only
static audit of scoreFoodWater.
Findings
1. The observed GCC-all-score-53 is CONSTRUCT-DETERMINISTIC, not a
regional-default leak. Pinned mathematically:
- IPC/HDX doesn't publish active food-crisis data for food-secure
states → scorer's fao-null branch imputes IMPUTE.ipcFood=88
(class='stable-absence', cov=0.7) at combined weight 0.6
- WB indicator ER.H2O.FWST.ZS (labelled 'water stress') for GCC
is EXTREME (KW ~3200%, BH ~3400%, UAE ~2080%, QA ~770%) — all
clamp to sub-score 0 under the scorer's lower-better 0..100
normaliser at weight 0.4
- Blended with peopleInCrisis=0 (fao block present with zero):
(100 * 0.45 + 0 * 0.4) / (0.45 + 0.4) = 45 / 0.85 ≈ 53
Every GCC country has the same inputs → same outputs. That's
construct math, not a regional lookup.
2. Indicator-keyword routing is code-correct. `'water stress'`,
`'withdrawal'`, `'dependency'` route to lower-better;
`'availability'`, `'renewable'`, `'access'` route to
higher-better; unrecognized indicators fall through to a
value-range heuristic with a WARN log.
3. No bug or methodology decision required. The 53-all-GCC output
is a correct summary statement: "non-crisis food security +
severe water-withdrawal stress." A future construct decision
might split foodWater into separate food and water dims so one
saturated sub-signal doesn't dominate the combined dim for
desert economies — but that's a construct redesign, not a bug.
Shipped
- `docs/methodology/known-limitations.md` — extended with a new
section documenting the foodWater audit findings, the exact
blend math that yields ~53 for GCC, cohort-determinism vs
regional-default, and a follow-up data-side spot-check list
gated on API-key access.
- `tests/resilience-foodwater-field-mapping.test.mts` — 8 new
regression-guard tests:
1. indicator='water stress' routes to lower-better
2. GCC extreme-withdrawal anchor (value=2000 → blended score 53)
3. indicator='renewable water availability' routes to higher-better
4. fao=null with static record → imputes 88; imputationClass=null
because observed AQUASTAT wins (weightedBlend T1.7 rule)
5. fully-imputed (fao=null + aquastat=null) surfaces
imputationClass='stable-absence'
6. static-record absent entirely → coverage=0, NOT impute
7. Cohort determinism — identical inputs → identical scores
8. Different water-profile inputs → different scores (rules
out regional-default hypothesis)
Verified
- `npx tsx --test tests/resilience-foodwater-field-mapping.test.mts` — 8 pass / 0 fail
- `npm run test:data` — 6711 pass / 0 fail (PR 5.2's 9 + PR 5.3's 8 = 17 new stacked)
- `npm run typecheck` / `typecheck:api` — green
- `npm run lint` / `lint:md` — clean
* fix(resilience): PR 5.3 review — pin IMPUTE branch for GCC anchor; fix comment math
Addresses 3 P2 Greptile findings on #3374 — all variations of the same
root cause: the test fixture + doc described two different code paths
that coincidentally both produce ~53 for GCC inputs.
Changes
1. GCC anchor test now drives the IMPUTE branch (`fao: null`), matching
what the static seeder emits for GCC in production. The else branch
(`fao: { peopleInCrisis: 0 }`) happens to converge on ~52.94 by
coincidence but is NOT the live code path for GCC.
2. Doc finding #4 updated to show the IMPUTE-branch math
`(88×0.6 + 0×0.4) / 1.0 = 52.8 → 53` and explicitly notes the
else-branch convergence as a coincidence — not the construct's
intent.
3. Comment math off-by-one fix at line 107:
(88×0.6 + 80×0.4) / (0.6+0.4)
= 52.8 + 32.0
= 84.8 → 85 (was incorrectly stated as 85.6 → 86)
Test assertion `>= 80 && <= 90` still accepts 85 so behaviour is
unchanged; this was a comment-only error that would have misled
anyone reproducing the math by hand.
Verified
- `npx tsx --test tests/resilience-foodwater-field-mapping.test.mts`
— 8 pass / 0 fail (IMPUTE-branch anchor test produces 53 as expected)
- `npm run lint:md` — clean
Also rebased onto updated #3373 (which landed a backtick-escape fix).