fix(resilience): PR 1 — widen Comtrade period to 4y + surface picked year#3372
Conversation
PR 1 of cohort-audit plan 2026-04-24-002. Unblocks UAE, Oman, Bahrain
(and any other late-reporter) on the importConcentration dimension.
Problem
- seed-recovery-import-hhi.mjs queries Comtrade with `period=Y-1,Y-2`
(currently "2025,2024"). Several reporters publish Comtrade 1-2y
behind — their 2024/2025 rows are empty while 2023 is populated.
- With no data in the queried window, parseRecords() returned [] for
the reporter, the seeder counted a "skip", the scorer fell through
to IMPUTE (score=50, coverage=0.3, imputationClass="unmonitored"),
and the cohort-sanity audit flagged AE as a coverage-outlier inside
the GCC — exactly the class of silent gap the audit is designed to
catch.
Fix
1. Widen the Comtrade period parameter to a 4-year window Y-1..Y-4
via a new `buildPeriodParam(now)` helper. On-time reporters still
pick their latest year via the existing completeness tiebreak in
parseRecords(); late reporters now pick up whatever year they
actually published in (2023 for UAE, etc.).
2. parseRecords() now returns { rows, year } — the year surfaces in
the per-country payload as `year: number | null` for operator
freshness audit. The scorer already expects this shape
(_dimension-scorers.ts:1524 RecoveryImportHhiCountry.year); this
PR actually populates it.
3. `buildPeriodParam` + `parseRecords` are exported so their unit
tests can pin year-selection behaviour without hitting Comtrade.
Note on PR 2 of the same plan
The plan calls out "PR 2 — externalDebtCoverage re-goalpost to
Greenspan-Guidotti" as unshipped. It IS shipped: commit 7f78a75
"PR 3 §3.5 point 3 — re-goalpost externalDebtCoverage (0..5 → 0..2)"
landed under the prior workstream 2026-04-22-001. The new construct
invariants in tests/resilience-construct-invariants.test.mts
(shipped in PR 0 / #3369) confirm score(ratio=0)=100, score(1)=50,
score(2)=0 against current main. PR 2 of the cohort-audit plan is a
no-op; I'll flag this on the plan review thread rather than bundle
a plan edit into this PR.
Verified
- `npx tsx --test tests/seed-recovery-import-hhi.test.mjs` — 19 pass
(10 existing + 9 new: buildPeriodParam shape; parseRecords picks
completeness-tiebreak, newer-year-on-ties, late-reporter fallback;
empty/negative/world-aggregate handling)
- `npx tsx --test tests/seed-comtrade-5xx-retry.test.mjs` — green
(the `{ records, status }` destructure pattern at the caller still
works; the new third field `year` is additive)
- `npm run test:data` — 6703 pass / 0 fail
- `npm run typecheck` / `typecheck:api` — green
- `npm run lint` / `lint:md` — no new warnings
- No cache-prefix bump: the payload shape only ADDS an optional
field; old snapshots remain valid readers.
Acceptance per plan
- Construct invariant: score(HHI=0.05) > score(HHI=0.20) — already
covered in tests/resilience-construct-invariants.test.mts (PR #3369)
- Monotonicity pin: score(hhi=0.15) > score(hhi=0.45) — already
covered in tests/resilience-dimension-monotonicity.test.mts
Post-deploy verification
After the next Railway seed-bundle-resilience-recovery cron tick,
confirm UAE/OM/BH appear in `resilience:recovery:import-hhi:v1`
with non-null hhi and `year` = 2023 (or their actual latest year).
Then re-run the cohort audit — the GCC coverage-outlier flag on
AE.importConcentration should disappear.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Greptile SummaryThis PR widens the Comtrade query window from 2 years to 4 years ( Confidence Score: 5/5Safe to merge — change is purely additive, all callers updated, and new behaviour is well-covered by tests. All findings are P2 or better. The logic change is narrow (wider Comtrade query window + surface the picked year), backward-compatible (adds an optional field to the Redis payload), and the tiebreak algorithm is correct. Tests cover all meaningful edge cases including the late-reporter scenario that motivated this fix. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant Cron as Railway Cron
participant Worker as runWorker
participant Fetch as fetchImportsForReporter
participant Parse as parseRecords
participant HHI as computeHhi
participant Redis as Upstash Redis
Cron->>Worker: start (queue of ISO-2 reporters)
loop per reporter
Worker->>Fetch: fetchImportsForReporter(unCode, apiKey)
Fetch->>Fetch: buildPeriodParam() → "Y-1,Y-2,Y-3,Y-4"
Fetch->>Fetch: GET Comtrade /data/v1/get/C/A/HS?period=Y-1..Y-4
Fetch->>Parse: parseRecords(json)
Note over Parse: group by period,<br/>usable = non-aggregate count,<br/>pick best (completeness ≻ recency)
Parse-->>Fetch: { rows, year }
Fetch-->>Worker: { records: rows, year, status }
Worker->>HHI: computeHhi(records)
Note over HHI: filter out partnerCode 0/000,<br/>compute Σ(share²)
HHI-->>Worker: { hhi, partnerCount } | null
alt result non-null
Worker->>Redis: countries[iso2] = { hhi, concentrated, partnerCount, year, fetchedAt }
else null / empty
Worker->>Worker: skipped++
end
end
Worker->>Redis: publish resilience:recovery:import-hhi:v1
Reviews (1): Last reviewed commit: "fix(resilience): widen Comtrade period t..." | Re-trigger Greptile |
Summary
PR 1 of the cohort-audit workstream (plan:
docs/plans/2026-04-24-002-fix-resilience-cohort-ranking-structural-audit-plan.md). Unblocks UAE, Oman, Bahrain (and any other late-reporter) on theimportConcentrationdimension.Follows PR #3369 (cohort-sanity release-gate harness).
Problem
scripts/seed-recovery-import-hhi.mjsqueries Comtrade withperiod=Y-1,Y-2— currently2025,2024. Several reporters (UAE, Oman, Bahrain, and others) publish Comtrade 1-2y behind G7 — their 2024/2025 rows are empty while 2023 is populated. With no data in the queried window:parseRecords()returned[]for the reporter,IMPUTE(score=50, coverage=0.3, imputationClass='unmonitored'),This is a clean seed-level fix — same family as
wb-bulk-mrv1-null-coverage-trap.Fix
buildPeriodParam(nowYear)helper. Query becomesperiod=Y-1,Y-2,Y-3,Y-4. On-time reporters still pick their newest year via the existing completeness tiebreak; late reporters pick up whatever year they actually published.parseRecords()now returns{ rows, year }— the picked year surfaces in the per-country payload asyear: number | nullfor operator freshness audit. The scorer'sRecoveryImportHhiCountrytype at_dimension-scorers.ts:1524already expects this field; PR 1 actually populates it.buildPeriodParamandparseRecordsare now exported so unit tests can pin year-selection behaviour without hitting Comtrade.No cache-prefix bump: the payload shape only ADDS an optional field; old snapshots remain valid readers.
Note on PR 2 of the plan
The plan calls out "PR 2 — externalDebtCoverage re-goalpost to Greenspan-Guidotti" as unshipped. It IS shipped: commit
7f78a7561(feat(resilience): PR 3 §3.5 point 3 — re-goalpost externalDebtCoverage (0..5 → 0..2)) landed under the prior workstream plan2026-04-22-001. My new construct invariants intests/resilience-construct-invariants.test.mts(PR #3369) confirmscore(ratio=0)=100,score(1)=50,score(2)=0against current main. PR 2 of the cohort-audit plan is a no-op — flagging this here rather than bundling a plan edit into PR 1.Testing
npx tsx --test tests/seed-recovery-import-hhi.test.mjs— 19 pass (10 existing + 9 new):buildPeriodParamshape: 4y window, Y-1..Y-4, never the current yearparseRecordspicks: completeness-tiebreak winner, newer-year on ties, late-reporter fallback (the UAE/OM/BH scenario)npx tsx --test tests/seed-comtrade-5xx-retry.test.mjs— green (the caller's{ records, status }destructure still works; the newyearfield is purely additive)npm run test:data— 6703 pass / 0 failnpm run typecheck/typecheck:api— greennpm run lint/lint:md— no new warningsbuild:full+ version sync — all greenAcceptance per plan:
score(HHI=0.05) > score(HHI=0.20)— already covered intests/resilience-construct-invariants.test.mts(feat(resilience): PR 0 — cohort-sanity release-gate harness #3369)tests/resilience-dimension-monotonicity.test.mtsPost-Deploy Monitoring & Validation
What to monitor/search
seed-bundle-resilience-recovery— specifically the[seed] import-hhi: N fetched, M skipped, E errors, T totallineresilience:recovery:import-hhi:v1payloadValidation checks (queries/commands)
Expected healthy behavior
seed-bundle-resilience-recoverystill ≤ 30min budget; totalfetchedcount INCREASES (late reporters now land data instead of skipping)resilience:recovery:import-hhi:v1has ayearfield on every populated country entry (range 2022..2025 depending on reporter)/api/healthremains HEALTHY onresilience.recovery.import-hhiscoreImportConcentrationreturns real scores (not IMPUTE) for UAE/OM/BHFailure signal(s) / rollback trigger
PERIOD_WINDOW_YEARSfrom 4 → 3 and redeployfetchedcount DROPS (indicates wider window caused per-reporter parse failures) — rollback: revert this PRyear— unlikely (the type was already optional) but rollback if seenValidation window & owner
Related
docs/plans/2026-04-24-002-fix-resilience-cohort-ranking-structural-audit-plan.mdwb-bulk-mrv1-null-coverage-trap(same family of silent late-reporter drops in bulk-query APIs)🤖 Generated with Claude Opus 4.7 (1M context) via Claude Code + Compound Engineering v2.49.0
Co-Authored-By: Claude Opus 4.7 (1M context) [email protected]