Skip to content

fix(resilience): PR 1 — widen Comtrade period to 4y + surface picked year#3372

Merged
koala73 merged 1 commit into
mainfrom
fix/resilience-pr1-hhi-period
Apr 24, 2026
Merged

fix(resilience): PR 1 — widen Comtrade period to 4y + surface picked year#3372
koala73 merged 1 commit into
mainfrom
fix/resilience-pr1-hhi-period

Conversation

@koala73

@koala73 koala73 commented Apr 24, 2026

Copy link
Copy Markdown
Owner

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 the importConcentration dimension.

Follows PR #3369 (cohort-sanity release-gate harness).

Problem

scripts/seed-recovery-import-hhi.mjs queries Comtrade with period=Y-1,Y-2 — currently 2025,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:

  1. parseRecords() returned [] for the reporter,
  2. seeder counted a "skip",
  3. scorer fell through to IMPUTE (score=50, coverage=0.3, imputationClass='unmonitored'),
  4. the cohort-sanity audit in PR feat(resilience): PR 0 — cohort-sanity release-gate harness #3369 correctly flagged AE as a coverage-outlier inside the GCC.

This is a clean seed-level fix — same family as wb-bulk-mrv1-null-coverage-trap.

Fix

  1. 4-year period window via a new buildPeriodParam(nowYear) helper. Query becomes period=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.
  2. parseRecords() now returns { rows, year } — the picked year surfaces in the per-country payload as year: number | null for operator freshness audit. The scorer's RecoveryImportHhiCountry type at _dimension-scorers.ts:1524 already expects this field; PR 1 actually populates it.
  3. ExportsbuildPeriodParam and parseRecords are 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 plan 2026-04-22-001. My new construct invariants in tests/resilience-construct-invariants.test.mts (PR #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 — flagging this here rather than bundling a plan edit into PR 1.

Testing

  • npx tsx --test tests/seed-recovery-import-hhi.test.mjs19 pass (10 existing + 9 new):
    • buildPeriodParam shape: 4y window, Y-1..Y-4, never the current year
    • parseRecords picks: completeness-tiebreak winner, newer-year on ties, late-reporter fallback (the UAE/OM/BH scenario)
    • Empty / negative-value / world-aggregate handling
  • npx tsx --test tests/seed-comtrade-5xx-retry.test.mjs — green (the caller's { records, status } destructure still works; the new year field is purely additive)
  • npm run test:data6703 pass / 0 fail
  • npm run typecheck / typecheck:api — green
  • npm run lint / lint:md — no new warnings
  • Pre-push: typecheck + build:full + version sync — all green

Acceptance per plan:

Post-Deploy Monitoring & Validation

What to monitor/search

  • Logs: Railway cron seed-bundle-resilience-recovery — specifically the [seed] import-hhi: N fetched, M skipped, E errors, T total line
  • Keys: resilience:recovery:import-hhi:v1 payload

Validation checks (queries/commands)

# After the next bundle cron tick (weekly — `0 6 * * 1`):
redis-cli --url $UPSTASH_REDIS_REST_URL GET resilience:recovery:import-hhi:v1 | \
  jq '.countries | to_entries | map(select(.value.hhi != null)) | length'
# Expected: higher than pre-fix count (late-reporters newly populated)

# Spot-check UAE/OM/BH specifically:
redis-cli --url $UPSTASH_REDIS_REST_URL GET resilience:recovery:import-hhi:v1 | \
  jq '.countries | {AE: .AE, OM: .OM, BH: .BH}'
# Expected: non-null hhi + `year` field surfaces the picked year (likely 2023 for AE/OM/BH)

# Re-run the cohort-sanity audit (PR #3369):
WORLDMONITOR_API_KEY=wm_xxx API_BASE=https://api.worldmonitor.app \
  node scripts/audit-resilience-cohorts.mjs | grep -A5 "Flagged patterns"
# Expected: GCC coverage-outlier flag on AE.importConcentration disappears

Expected healthy behavior

  • seed-bundle-resilience-recovery still ≤ 30min budget; total fetched count INCREASES (late reporters now land data instead of skipping)
  • resilience:recovery:import-hhi:v1 has a year field on every populated country entry (range 2022..2025 depending on reporter)
  • /api/health remains HEALTHY on resilience.recovery.import-hhi
  • Scorer's scoreImportConcentration returns real scores (not IMPUTE) for UAE/OM/BH

Failure signal(s) / rollback trigger

  • Comtrade rate-limit errors spike (if the wider query pushes per-key usage over a threshold) — mitigation: drop PERIOD_WINDOW_YEARS from 4 → 3 and redeploy
  • fetched count DROPS (indicates wider window caused per-reporter parse failures) — rollback: revert this PR
  • New NullPointer / undefined-field errors surface in scorer logs mentioning year — unlikely (the type was already optional) but rollback if seen

Validation window & owner

  • Window: first 2 Railway cron cycles after merge (~2 weeks)
  • Owner: Elie (@eliehabib)

Related


🤖 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]

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

vercel Bot commented Apr 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Apr 24, 2026 9:04am

Request Review

@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR widens the Comtrade query window from 2 years to 4 years (Y-1..Y-4) to unblock late-reporting countries (UAE, Oman, Bahrain) that publish Comtrade data 1–2 years behind G7 reporters, and surfaces the picked data year in the Redis payload for freshness auditing. The refactor is clean — buildPeriodParam and parseRecords are extracted and exported, the return shape of parseRecords is updated from a bare array to { rows, year }, and all callers are updated accordingly. Test coverage is thorough (9 new tests covering tiebreak behaviour, late-reporter fallback, world-aggregate exclusion, and null/empty paths).

Confidence Score: 5/5

Safe 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

Filename Overview
scripts/seed-recovery-import-hhi.mjs Core seeder — adds buildPeriodParam (4-year window), refactors parseRecords to return { rows, year } with a completeness+recency tiebreak, and propagates year through fetchImportsForReporter and runWorker into the Redis payload. Logic is correct; world-aggregate exclusion in the usable count is consistent with computeHhi's downstream filter.
tests/seed-recovery-import-hhi.test.mjs Adds 9 tests for the new helpers covering 4-year window shape, completeness tiebreak, recency tiebreak, late-reporter (UAE/OM/BH) fallback, empty/null input, negative-value filtering, and world-aggregate exclusion from the usable count. All cases are behaviorally distinct and sufficient.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "fix(resilience): widen Comtrade period t..." | Re-trigger Greptile

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.

1 participant