Skip to content

feat(imf-weo): Sprint 4 IMF cohort — content-age (forecast-year semantics, 18mo budget)#3604

Merged
koala73 merged 3 commits into
mainfrom
feat/health-readiness-content-age-sprint-4-imf-weo-cohort
May 8, 2026
Merged

feat(imf-weo): Sprint 4 IMF cohort — content-age (forecast-year semantics, 18mo budget)#3604
koala73 merged 3 commits into
mainfrom
feat/health-readiness-content-age-sprint-4-imf-weo-cohort

Conversation

@koala73

@koala73 koala73 commented May 5, 2026

Copy link
Copy Markdown
Owner

Closes the deferred IMF/WEO portion of Sprint 4 of the 2026-05-04 health-readiness probe plan. Plan §477-485 listed "plus IMF/WEO/etc." as part of the annual-data migration. Branched off Sprint 1 (#3596) as a parallel sibling.

Summary

Migrates all 4 IMF SDMX seeders in one PR (they share the same upstream and same publication cadence):

  • `seed-imf-external.mjs` (BCA, TM_RPCH, TX_RPCH)
  • `seed-imf-growth.mjs` (NGDP_RPCH, NGDPDPC, NGDP_R, PPPPC, PPPGDP, NID_NGDP, NGSD_NGDP)
  • `seed-imf-labor.mjs` (LUR, LP)
  • `seed-imf-macro.mjs` (PCPIPCH, BCA_NGDPD, GGR_NGDP, PCPI, PCPIEPCH, GGX_NGDP, GGXONLB_NGDP)

New helper `scripts/_imf-weo-content-age-helpers.mjs`: `imfForecastYearToMs`, `imfWeoContentMeta` (with injectable `nowMs`), `IMF_WEO_MAX_CONTENT_AGE_MIN`. All 4 seeders import it with the same constant.

Why a separate helper from the WB cohort

WB and IMF have superficially identical `{countries: {ISO2: {year}}}` shapes but opposite `year` semantics:

WB indicators (LOSS, FOSL, NUCL, RNEW, HYRO) IMF/WEO (BCA, NGDP_RPCH, LUR, PCPI, ...)
What `year` means Observation year (latest data point's year) Forecast horizon (`weoYears()` = current + 2 prior; `latestValue()` picks first non-null)
In May 2026, max year typically 2024 (WB observes year-2 lag) 2026 (IMF forecasts current year)
End-of-year mapping `Date.UTC(2024, 11, 31)` = past — works `Date.UTC(2026, 11, 31)` = 7mo future — would be rejected by 1h skew limit, every fresh IMF cache reports STALE_CONTENT

The IMF helper maps year → end-of-(year - 1) UTC ms. Reads as: "the latest fully-observed period this forecast vintage is built on." For year=2026 → end-of-2025 = ~5mo ago in May 2026 → correctly fresh.

A dedicated test (`semantic difference from WB cohort: forecast year 2026 in May 2026 maps to past (NOT future)`) exists specifically to prevent a future refactor from accidentally collapsing the WB and IMF helpers.

Why one shared budget across all 4 IMF seeders (NOT per-seeder)

The WB cohort needed per-seeder budgets because publication lags differed (LOSS at ~17mo, FOSL at ~29mo). But all 4 IMF seeders fetch from the IDENTICAL upstream — IMF SDMX/WEO. WEO publishes April + October vintages each year as a single integrated release covering all WorldMonitor's indicator codes. So all 4 share the same fresh-arrival lag and steady-state ceiling. One budget = correct.

18-month budget — derivation

Steady-state model under "year → end-of-(year-1)" mapping:

  • After April N release: max year = N → newestItemAt = end-of-(N-1). Age = ~5mo.
  • After October N: max year still = N → age = ~11mo.
  • Just before April N+1: max year still = N → age = ~16mo. Steady-state ceiling.
  • After April N+1: max year advances to N+1 → resets.

Budget = 16mo ceiling + 2mo slack = 18 months. Trips when a full year of WEO releases is missed (both April AND October vintages of one year), which is the right pager threshold for an IMF outage.

Test plan

  • `node --test tests/imf-weo-content-age.test.mjs` → 15/15 pass
  • `npx tsx --test tests/imf-country-data.test.mts tests/seed-imf-extended.test.mjs tests/imf-weo-content-age.test.mjs` → 34/34 pass (existing IMF tests + new content-age, no regressions)
  • `node --test tests/imf-weo-content-age.test.mjs tests/seed-content-age-contract.test.mjs tests/health-content-age.test.mjs tests/seed-utils-empty-data-failure.test.mjs` → 47/47
  • `npm run typecheck:api` clean
  • `npm run lint` clean
  • Fresh-arrival regression guard: April 2026 vintage (max year 2026, age ~5mo) does NOT trip
  • Steady-state regression guard: just-before-April-2027 (max year 2026, age ~16mo) does NOT trip
  • Catastrophic stall: max year 2024 in May 2026 (age ~29mo) trips STALE_CONTENT — both 2025 + 2026 WEO missed
  • WB/IMF semantic-difference guard: prevents future refactor from collapsing the two helpers
  • Future-skew defense: forecast year 2099 (or 2027 if seeder ever extends `weoYears()` to currentYear+1) → end-of-(year-1) is still future → rejected
  • Late-reporter cohort: G7 freshness drives newestItemAt; lagging members (Venezuela, Eritrea) don't cause false-positive page
  • No seed-imf-*.mjs is in Dockerfile.relay (verified via grep) — no relay-COPY change needed
  • Post-merge: confirm `/api/health` snapshot shows `imfExternal`, `imfGrowth`, `imfLabor`, `imfMacro` with `contentAge` block populated and `status: OK`

Sprint 4 status after this PR

Plan §477-485 fully closed. The plan's "Definition of done" §530 (≥1 annual-data migrated) was satisfied by #3602; this PR + #3603 round out the rest of the listed cohort.

@vercel

vercel Bot commented May 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment May 8, 2026 9:03am

Request Review

@greptile-apps

greptile-apps Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Wires content-age health monitoring into all four IMF SDMX/WEO seeders via a new shared helper module (_imf-weo-content-age-helpers.mjs). The helper correctly encodes IMF's forecast-year semantics (year N → end-of-(N−1)) to avoid the future-dated rejection that WB-style end-of-year math would produce for current-year forecasts, and sets an 18-month budget derived from the April + October WEO release cadence.

  • New helper: imfForecastYearToMs maps forecast year N to Date.UTC(N-1, 11, 31, 23, 59, 59, 999); imfWeoContentMeta iterates the per-country dict with a 1-hour clock-skew guard; IMF_WEO_MAX_CONTENT_AGE_MIN = 18 × 30 × 24 × 60 minutes encodes the 16-month steady-state ceiling plus 2-month slack.
  • Four seeders (external, growth, labor, macro) each gain two-line import + contentMeta/maxContentAgeMin opt-in — no other changes.
  • Tests: 15 deterministic tests pinned to FIXED_NOW = 2026-05-05T12:00Z, covering invalid shapes, skew rejection, fresh-arrival regression, steady-state ceiling, catastrophic-stall trip, and the WB/IMF semantic-divergence guard.

Confidence Score: 5/5

Safe to merge; the helper and seeders are additive opt-ins with no changes to existing fetch, parse, or publish logic.

The forecast-year to end-of-(year-1) math is correct and well-tested; the 1-hour skew guard is properly defensive; contentMeta is called with a defaulting Date.now() second parameter which is correct for production; the 18-month budget derivation is clearly documented and validated by the steady-state and catastrophic-stall tests. No existing code paths were modified.

No files require special attention. The helper module is the logical core and its arithmetic and guard conditions check out.

Important Files Changed

Filename Overview
scripts/_imf-weo-content-age-helpers.mjs New helper module: forecast year → end-of-(year-1) mapping plus 18-month budget constant; logic and guard rails are correct.
scripts/seed-imf-external.mjs Adds contentMeta/maxContentAgeMin wiring for the BCA/TM_RPCH/TX_RPCH indicators; no behavioural regressions introduced.
scripts/seed-imf-growth.mjs Same two-line import + contentMeta/maxContentAgeMin opt-in as the other three seeders; consistent with the cohort pattern.
scripts/seed-imf-labor.mjs Same pattern; wires LUR/LP indicators into the shared IMF content-age contract without issues.
scripts/seed-imf-macro.mjs Same pattern for the 7 macro indicators; consistent and correct.
tests/imf-weo-content-age.test.mjs 15 tests covering: unit conversions, null/invalid shapes, clock-skew rejection, fresh-arrival guard, steady-state ceiling guard, catastrophic-stall trip, WB/IMF semantic divergence, and late-reporter cohort behaviour.

Reviews (1): Last reviewed commit: "feat(imf-weo): Sprint 4 IMF cohort — con..." | Re-trigger Greptile

koala73 added a commit that referenced this pull request May 7, 2026
…st metric

Codex PR #3604 P2. The four IMF/WEO seeders write `entry.year` as the
priority-first non-null indicator's year (`ca?.year ?? tm?.year ?? tx?.year`
in seed-imf-external). That's correct as the public payload's "primary
metric vintage" but WRONG for content-age: a row with BCA=2024 +
import-volume=2026 publishes year=2024, even though the country dict
carries a fresh 2026 metric — content-age maps it to 2023-12-31 (~17mo old,
near-stale) when it actually carries a 2026 metric (~5mo old in May 2026).

Fix path A (preserves public payload semantics): seeders now populate a
dedicated `latestYear` field via a new `maxIntegerYear()` helper, computed
across ALL the country's indicator years. The content-age helper prefers
`entry.latestYear` over `entry.year`, falling back to `year` for back-compat
with caches written before this PR.

- scripts/_imf-weo-content-age-helpers.mjs — export `maxIntegerYear()`;
  `imfWeoContentMeta` reads `entry.latestYear` first
- scripts/seed-imf-{external,growth,labor,macro}.mjs — populate `latestYear`
  alongside existing `year` (no public payload change beyond the new field)
- tests/imf-weo-content-age.test.mjs — add maxIntegerYear unit tests +
  three mixed-indicator-year regression tests covering the fresh-metric-
  behind-stale-primary case, latestYear=null fallback, and heterogeneous
  cohort newest/oldest extraction
koala73 added a commit that referenced this pull request May 8, 2026
… guard + schemaVersion bump

PR #3604 review findings #1 + #2. Both advisory, no behavior change today.

#1 Horizon-extension trap: weoYears() currently returns [currentYear,
   currentYear-1, currentYear-2], so max year = currentYear and the 1h
   skew filter is purely defensive. If a future Sprint extends weoYears()
   to include currentYear+1 to surface forward forecasts, the skew filter
   would silently drop every fresh +1 entry, regressing cohort
   newestItemAt to the prior year and producing FALSE STALE_CONTENT for
   genuinely-fresh data. Added load-bearing comment near the skew check
   plus a regression-guard test that documents the trap shape under
   FIXED_NOW=2026-05-05. Test asserts the trap, not desired behavior;
   when horizon extension lands the test fails and forces revisit.

#2 schemaVersion bump 1->2 across all 4 seeders. Codex P2 added the
   latestYear field; envelope newestItemAt math now differs under the
   same schema number. Bumping forces a clean republish on rollout and
   makes rollback observable rather than silently drifting envelope math
   while caches keep the new shape.
Base automatically changed from feat/health-readiness-content-age-sprint-1 to main May 8, 2026 08:43
koala73 added 3 commits May 8, 2026 12:55
…tics, 18mo budget)

Closes the deferred IMF/WEO portion of Sprint 4 (plan §477-485 listed
"plus IMF/WEO/etc." as part of the annual-data migration). Branched off
Sprint 1 (#3596) as a parallel sibling.

Migrates all 4 IMF SDMX seeders in one PR:
  - seed-imf-external.mjs   (BCA, TM_RPCH, TX_RPCH)
  - seed-imf-growth.mjs     (NGDP_RPCH, NGDPDPC, NGDP_R, PPPPC, PPPGDP, NID_NGDP, NGSD_NGDP)
  - seed-imf-labor.mjs      (LUR, LP)
  - seed-imf-macro.mjs      (PCPIPCH, BCA_NGDPD, GGR_NGDP, PCPI, PCPIEPCH, GGX_NGDP, GGXONLB_NGDP)

## The semantic difference from WB cohort (and why a separate helper)

WB indicators store the OBSERVED year — `record.date = "2024"` means
data observed during calendar year 2024. The WB helper maps year →
end-of-year UTC ms (the latest observation date inside the named year).

IMF/WEO stores the FORECAST horizon, NOT an observation year. The
`weoYears()` function in `_seed-utils.mjs` returns
`[currentYear, currentYear-1, currentYear-2]` and `latestValue()` picks
the first year that has a finite value. So in May 2026 after the April
2026 WEO release, max stored year = 2026 — that's IMF's freshest
*forecast* for fiscal 2026, not observations through end-of-2026.

If the IMF helper reused the WB cohort helper (`yearToEndOfYearMs`):
year=2026 → end-of-2026 = Dec 31 2026 = ~7 months FUTURE relative to
NOW → rejected by 1h skew limit → `contentMeta` returns null → every
fresh IMF cache reports STALE_CONTENT. That's the failure mode this
module avoids.

Mapping rationale: `imfForecastYearToMs(year)` returns
`Date.UTC(year - 1, 11, 31, 23, 59, 59, 999)`. Reads as: "the latest
fully-observed period this forecast vintage is built on." For year=2026
→ end-of-2025 = ~5 months ago in May 2026. Correctly fresh.

A dedicated test (`semantic difference from WB cohort: forecast year
2026 in May 2026 maps to past (NOT future)`) exists specifically to
prevent a future refactor from collapsing the WB and IMF helpers.

## Why one shared budget across all 4 IMF seeders (NOT per-seeder)

WB cohort had per-seeder budgets because publication lags differed
(LOSS at ~17mo, FOSL at ~29mo). All 4 IMF seeders use the IDENTICAL
upstream — IMF SDMX/WEO. WEO publishes April + October vintages each
year as a single integrated release covering all WorldMonitor's
indicator codes. So all 4 share the same fresh-arrival lag and the
same steady-state ceiling. One budget = correct.

## 18-month budget — derivation

Steady-state model under "year → end-of-(year-1)" mapping:

  - After April N release: max year = N → newestItemAt = end-of-(N-1).
    Age = ~5 months.
  - After October N: max year still = N → age = ~11 months.
  - Just before April N+1: max year still = N → age = ~16 months.
  - After April N+1: max year advances to N+1 → newestItemAt resets.

Steady-state ceiling = 16mo (just before April release of next year).
Budget = 16mo + 2mo slack = 18 months. Trips when a full year of WEO
releases is missed (both April AND October vintages of one year), which
is the right pager threshold for an IMF outage.

## Verification

  - 15/15 imf-weo content-age tests pass (incl. fresh-arrival + steady-
    state regression guards, future-skew defense, late-reporter cohort
    handling, and the WB-vs-IMF semantic-difference guard test)
  - Tested with `npx tsx --test` against the existing IMF test suites:
    34/34 across `imf-country-data` + `seed-imf-extended` + new file
  - 47/47 across Sprint 1 + IMF cohort stack
  - typecheck:api clean; lint clean
  - Zero seed-imf-*.mjs files in Dockerfile.relay (verified via grep)
    so no relay-COPY change needed

## Sprint 4 status after this PR

  - ✅ power-reliability (#3602)
  - ✅ low-carbon-generation + fossil-electricity-share (#3603)
  - ✅ IMF/WEO cohort: external + growth + labor + macro (this PR)

Plan §477-485 fully closed. The plan's "Definition of done" §530
(≥1 annual-data migrated) was satisfied by #3602; this PR + #3603
round out the rest of the listed cohort.
…st metric

Codex PR #3604 P2. The four IMF/WEO seeders write `entry.year` as the
priority-first non-null indicator's year (`ca?.year ?? tm?.year ?? tx?.year`
in seed-imf-external). That's correct as the public payload's "primary
metric vintage" but WRONG for content-age: a row with BCA=2024 +
import-volume=2026 publishes year=2024, even though the country dict
carries a fresh 2026 metric — content-age maps it to 2023-12-31 (~17mo old,
near-stale) when it actually carries a 2026 metric (~5mo old in May 2026).

Fix path A (preserves public payload semantics): seeders now populate a
dedicated `latestYear` field via a new `maxIntegerYear()` helper, computed
across ALL the country's indicator years. The content-age helper prefers
`entry.latestYear` over `entry.year`, falling back to `year` for back-compat
with caches written before this PR.

- scripts/_imf-weo-content-age-helpers.mjs — export `maxIntegerYear()`;
  `imfWeoContentMeta` reads `entry.latestYear` first
- scripts/seed-imf-{external,growth,labor,macro}.mjs — populate `latestYear`
  alongside existing `year` (no public payload change beyond the new field)
- tests/imf-weo-content-age.test.mjs — add maxIntegerYear unit tests +
  three mixed-indicator-year regression tests covering the fresh-metric-
  behind-stale-primary case, latestYear=null fallback, and heterogeneous
  cohort newest/oldest extraction
… guard + schemaVersion bump

PR #3604 review findings #1 + #2. Both advisory, no behavior change today.

#1 Horizon-extension trap: weoYears() currently returns [currentYear,
   currentYear-1, currentYear-2], so max year = currentYear and the 1h
   skew filter is purely defensive. If a future Sprint extends weoYears()
   to include currentYear+1 to surface forward forecasts, the skew filter
   would silently drop every fresh +1 entry, regressing cohort
   newestItemAt to the prior year and producing FALSE STALE_CONTENT for
   genuinely-fresh data. Added load-bearing comment near the skew check
   plus a regression-guard test that documents the trap shape under
   FIXED_NOW=2026-05-05. Test asserts the trap, not desired behavior;
   when horizon extension lands the test fails and forces revisit.

#2 schemaVersion bump 1->2 across all 4 seeders. Codex P2 added the
   latestYear field; envelope newestItemAt math now differs under the
   same schema number. Bumping forces a clean republish on rollout and
   makes rollback observable rather than silently drifting envelope math
   while caches keep the new shape.
@koala73
koala73 force-pushed the feat/health-readiness-content-age-sprint-4-imf-weo-cohort branch from 40d67eb to 11425f2 Compare May 8, 2026 09:00
@koala73
koala73 merged commit ca63977 into main May 8, 2026
11 checks passed
@koala73
koala73 deleted the feat/health-readiness-content-age-sprint-4-imf-weo-cohort branch May 8, 2026 09:07
fuleinist pushed a commit to fuleinist/worldmonitor that referenced this pull request May 9, 2026
…tics, 18mo budget) (koala73#3604)

* feat(imf-weo): Sprint 4 IMF cohort — content-age (forecast-year semantics, 18mo budget)

Closes the deferred IMF/WEO portion of Sprint 4 (plan §477-485 listed
"plus IMF/WEO/etc." as part of the annual-data migration). Branched off
Sprint 1 (koala73#3596) as a parallel sibling.

Migrates all 4 IMF SDMX seeders in one PR:
  - seed-imf-external.mjs   (BCA, TM_RPCH, TX_RPCH)
  - seed-imf-growth.mjs     (NGDP_RPCH, NGDPDPC, NGDP_R, PPPPC, PPPGDP, NID_NGDP, NGSD_NGDP)
  - seed-imf-labor.mjs      (LUR, LP)
  - seed-imf-macro.mjs      (PCPIPCH, BCA_NGDPD, GGR_NGDP, PCPI, PCPIEPCH, GGX_NGDP, GGXONLB_NGDP)

## The semantic difference from WB cohort (and why a separate helper)

WB indicators store the OBSERVED year — `record.date = "2024"` means
data observed during calendar year 2024. The WB helper maps year →
end-of-year UTC ms (the latest observation date inside the named year).

IMF/WEO stores the FORECAST horizon, NOT an observation year. The
`weoYears()` function in `_seed-utils.mjs` returns
`[currentYear, currentYear-1, currentYear-2]` and `latestValue()` picks
the first year that has a finite value. So in May 2026 after the April
2026 WEO release, max stored year = 2026 — that's IMF's freshest
*forecast* for fiscal 2026, not observations through end-of-2026.

If the IMF helper reused the WB cohort helper (`yearToEndOfYearMs`):
year=2026 → end-of-2026 = Dec 31 2026 = ~7 months FUTURE relative to
NOW → rejected by 1h skew limit → `contentMeta` returns null → every
fresh IMF cache reports STALE_CONTENT. That's the failure mode this
module avoids.

Mapping rationale: `imfForecastYearToMs(year)` returns
`Date.UTC(year - 1, 11, 31, 23, 59, 59, 999)`. Reads as: "the latest
fully-observed period this forecast vintage is built on." For year=2026
→ end-of-2025 = ~5 months ago in May 2026. Correctly fresh.

A dedicated test (`semantic difference from WB cohort: forecast year
2026 in May 2026 maps to past (NOT future)`) exists specifically to
prevent a future refactor from collapsing the WB and IMF helpers.

## Why one shared budget across all 4 IMF seeders (NOT per-seeder)

WB cohort had per-seeder budgets because publication lags differed
(LOSS at ~17mo, FOSL at ~29mo). All 4 IMF seeders use the IDENTICAL
upstream — IMF SDMX/WEO. WEO publishes April + October vintages each
year as a single integrated release covering all WorldMonitor's
indicator codes. So all 4 share the same fresh-arrival lag and the
same steady-state ceiling. One budget = correct.

## 18-month budget — derivation

Steady-state model under "year → end-of-(year-1)" mapping:

  - After April N release: max year = N → newestItemAt = end-of-(N-1).
    Age = ~5 months.
  - After October N: max year still = N → age = ~11 months.
  - Just before April N+1: max year still = N → age = ~16 months.
  - After April N+1: max year advances to N+1 → newestItemAt resets.

Steady-state ceiling = 16mo (just before April release of next year).
Budget = 16mo + 2mo slack = 18 months. Trips when a full year of WEO
releases is missed (both April AND October vintages of one year), which
is the right pager threshold for an IMF outage.

## Verification

  - 15/15 imf-weo content-age tests pass (incl. fresh-arrival + steady-
    state regression guards, future-skew defense, late-reporter cohort
    handling, and the WB-vs-IMF semantic-difference guard test)
  - Tested with `npx tsx --test` against the existing IMF test suites:
    34/34 across `imf-country-data` + `seed-imf-extended` + new file
  - 47/47 across Sprint 1 + IMF cohort stack
  - typecheck:api clean; lint clean
  - Zero seed-imf-*.mjs files in Dockerfile.relay (verified via grep)
    so no relay-COPY change needed

## Sprint 4 status after this PR

  - ✅ power-reliability (koala73#3602)
  - ✅ low-carbon-generation + fossil-electricity-share (koala73#3603)
  - ✅ IMF/WEO cohort: external + growth + labor + macro (this PR)

Plan §477-485 fully closed. The plan's "Definition of done" §530
(≥1 annual-data migrated) was satisfied by koala73#3602; this PR + koala73#3603
round out the rest of the listed cohort.

* fix(imf-weo): use max forecast year for content-age, not priority-first metric

Codex PR koala73#3604 P2. The four IMF/WEO seeders write `entry.year` as the
priority-first non-null indicator's year (`ca?.year ?? tm?.year ?? tx?.year`
in seed-imf-external). That's correct as the public payload's "primary
metric vintage" but WRONG for content-age: a row with BCA=2024 +
import-volume=2026 publishes year=2024, even though the country dict
carries a fresh 2026 metric — content-age maps it to 2023-12-31 (~17mo old,
near-stale) when it actually carries a 2026 metric (~5mo old in May 2026).

Fix path A (preserves public payload semantics): seeders now populate a
dedicated `latestYear` field via a new `maxIntegerYear()` helper, computed
across ALL the country's indicator years. The content-age helper prefers
`entry.latestYear` over `entry.year`, falling back to `year` for back-compat
with caches written before this PR.

- scripts/_imf-weo-content-age-helpers.mjs — export `maxIntegerYear()`;
  `imfWeoContentMeta` reads `entry.latestYear` first
- scripts/seed-imf-{external,growth,labor,macro}.mjs — populate `latestYear`
  alongside existing `year` (no public payload change beyond the new field)
- tests/imf-weo-content-age.test.mjs — add maxIntegerYear unit tests +
  three mixed-indicator-year regression tests covering the fresh-metric-
  behind-stale-primary case, latestYear=null fallback, and heterogeneous
  cohort newest/oldest extraction

* chore(imf-weo): adversarial-review hardening — horizon-extension trap guard + schemaVersion bump

PR koala73#3604 review findings #1 + #2. Both advisory, no behavior change today.

#1 Horizon-extension trap: weoYears() currently returns [currentYear,
   currentYear-1, currentYear-2], so max year = currentYear and the 1h
   skew filter is purely defensive. If a future Sprint extends weoYears()
   to include currentYear+1 to surface forward forecasts, the skew filter
   would silently drop every fresh +1 entry, regressing cohort
   newestItemAt to the prior year and producing FALSE STALE_CONTENT for
   genuinely-fresh data. Added load-bearing comment near the skew check
   plus a regression-guard test that documents the trap shape under
   FIXED_NOW=2026-05-05. Test asserts the trap, not desired behavior;
   when horizon extension lands the test fails and forces revisit.

#2 schemaVersion bump 1->2 across all 4 seeders. Codex P2 added the
   latestYear field; envelope newestItemAt math now differs under the
   same schema number. Bumping forces a clean republish on rollout and
   makes rollback observable rather than silently drifting envelope math
   while caches keep the new shape.
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