Skip to content

Fall back to a 2022–2036 date range before metadata loads - #1266

Open
DTrim99 wants to merge 2 commits into
mainfrom
metadata-fallback-2036
Open

DTrim99 wants to merge 2 commits into
mainfrom
metadata-fallback-2036

Conversation

@DTrim99

@DTrim99 DTrim99 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part of the US 2036 extension, tracked in PolicyEngine/policyengine-api#3926.

Until API metadata loads, getDateRange falls back to a fixed range. This PR moves the fallback's last day from 2035-12-31 to 2036-12-31.

Everything else in the app reads its years from economy_options.time_period in the API metadata. 2036 reaches the year pickers once the API lists it in PolicyEngine/policyengine-api#3928. That PR stays a draft until a bundle carries a 2036 SPM forecast. The fallback applies only until metadata arrives, so merging this before #3928 changes nothing that users see. It is held under the merge hold in PolicyEngine/policyengine-api#3926 until spm-calculator 1.1.0 is on PyPI and 2036 runs end to end.

The fallback is shared with the UK and is not shown in today's UI: the date controls that read it render only after metadata loads.

Files

  • app/src/libs/metadataUtils.ts: the fallback maxDate is now '2036-12-31'. A comment says it applies only until metadata loads and is shared by the US and the UK.
  • app/src/tests/unit/libs/metadataUtils.test.ts: five getDateRange tests.
    • Loaded metadata through 2036, through 2035 (the US today) and 2024–2030 (the UK today). Each gives its own range, not the fallback.
    • An empty year list, and the store while the metadata request is in flight. Both give the fallback.
  • app/src/tests/fixtures/libs/metadataUtilsMocks.ts: adds mockStateWithTimePeriods, mockStateWhileMetadataLoads (the reducer's pending state) and the tests' constants. mockStateWithMetadata now types its overrides as Partial<MetadataApiPayload['result']>, the object it spreads them into. The old type, Partial<MetadataApiPayload>, rejects economy_options under tsc (TS2353). No earlier caller passed overrides.

Test plan

  • Vitest, src/tests/unit/libs/metadataUtils.test.ts: 43 passed. That is 40 after the rebase, which picked up 14 search-visibility tests from main, plus 3 new getDateRange cases.
  • Prettier and ESLint (--max-warnings 0) are clean on the three files.
  • tsc --noEmit is clean on the test and fixture files. This used a narrowed config; CI runs the full project typecheck.
  • After Offer 2036 as a US year in economy options and the v2 catalog policyengine-api#3928 merges, check on a preview against staging that the year picker shows 2036.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
policyengine-calculator-next Ready Ready Preview Oct 27, 2026 1:18pm UTC
policyengine-website Ready Ready Preview Oct 27, 2026 1:18pm UTC

Request Review

getDateRange falls back to a fixed range until API metadata arrives. Move
its last day to 2036-12-31 for the US 2036 extension
(PolicyEngine/policyengine-api#3926). Everything else reads the years
from economy_options.time_period.

mockStateWithMetadata now types its overrides as the payload result,
which is where it spreads them. The old Partial<MetadataApiPayload> type
rejected economy_options under tsc.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@PavelMakarchuk PavelMakarchuk left a comment

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.

Code Review: PR #1266 "Fall back to a 2022–2036 date range before metadata loads" (@DTrim99, round 1)

ID legend: C = critical (must fix), SA = should address, G = suggestion.

Source Documents

  • Reviewed head SHA: 7e9e173
  • Mode: full
  • Read: the diff (3 files, +51/-2); the four callers of getDateRange (app/src/components/flagship/reform/DraftTable.tsx:174, app/src/components/flagship/reform/ProvisionDetails.tsx:103, app/src/pages/reportBuilder/modals/PolicyCreationModal.tsx:101, app/src/pathways/report/components/PolicyParameterSelectorValueSetter.tsx:35) and the gates that decide when they render; app/src/reducers/metadataReducer.ts; the repo rules in docs/engineering/skills/testing.md and docs/engineering/skills/repository-guidance.md; PolicyEngine/policyengine-api#3926 and #3928; spm-calculator#51; PyPI spm-calculator.
  • API metadata ranges (live /us/metadata and /uk/metadata, 2026-10-27): US time_period 2022–2035 (model 2.2.1); UK 2024–2030 (model 2.132.6).
  • Runs: the changed test file, app/src/tests/unit/libs/, the whole app/ suite, 8 mutants of getDateRange, Prettier, ESLint and tsc --noEmit, at the head and with the fixes below applied. Local toolchain: Node 22.23.1, bun 1.3.9 (CI uses Node 26; these tests do not format numbers).

Branch Status

The branch is 1 commit ahead of main and 4 commits behind. None of the 4 commits touch the three changed files, the metadata reducer or the four callers, and git merge-tree is clean. GitHub reports the PR as mergeable. A rebase is not necessary for this review.

What the PR gets right

  • The fallback lives in one place (app/src/libs/metadataUtils.ts:38), and the change is one line. '2036-12-31' is inclusive and matches the metadata branch (${last}-12-31), so there is no off-by-one.
  • The fixture type fix is correct. mockStateWithMetadata spreads overrides into result, the old type gives TS2353, and all 11 earlier callers pass no overrides.
  • The tests follow the repo rules: fixtures in src/tests/fixtures/, grouped constants, Given-When-Then names. The unsorted [2036, 2035, 2022] input also exercises the sort.
  • No changelog is needed. The repo removed its changelog file in 9e0c5976.

Critical (Must Fix)

None.

Should Address

SA1. The "metadata through 2036" test cannot tell metadata from the fallback

  • Where: app/src/tests/fixtures/libs/metadataUtilsMocks.ts:35-38, app/src/tests/unit/libs/metadataUtils.test.ts:281-290.
  • Evidence: EXPECTED_DATE_RANGES.THROUGH_2036 and EXPECTED_DATE_RANGES.FALLBACK are the same value (2022-01-01 to 2036-12-31). A mutant that always returns the fallback passes all 40 tests in the changed file. In the whole suite, only an unrelated component test catches it (app/src/tests/unit/components/flagship/reform/DraftTable.test.tsx:170). A second mutant that fixes the metadata lower bound at 2022-01-01 passes the whole suite (5901/5901), because no existing test checks the lower bound of a range that starts after 2022.
  • Why this severity: the new behavior is "2036 only until metadata loads". The tests pin the "2036" half but not the "only until" half, and the PR body uses that half to argue that an early merge is safe. This is a missing test for new behavior. It is not critical: the code is correct, and the whole suite already catches the first mutant.
  • Fix (passes with Prettier, ESLint and full tsc --noEmit clean; kills both mutants). The two new cases mirror today's API:
// app/src/tests/fixtures/libs/metadataUtilsMocks.ts: replace the two constant groups
export const TEST_TIME_PERIOD_YEARS = {
  THROUGH_2036: [2036, 2035, 2022],
  // The US API's years today: 2022 to 2035, newest first
  US_THROUGH_2035: [2035, 2034, 2022],
  // The UK API's years today: 2024 to 2030
  UK_2024_TO_2030: [2024, 2025, 2030],
  NONE: [],
} as const;

export const EXPECTED_DATE_RANGES = {
  THROUGH_2036: { minDate: '2022-01-01', maxDate: '2036-12-31' },
  US_THROUGH_2035: { minDate: '2022-01-01', maxDate: '2035-12-31' },
  UK_2024_TO_2030: { minDate: '2024-01-01', maxDate: '2030-12-31' },
  FALLBACK: { minDate: '2022-01-01', maxDate: '2036-12-31' },
} as const;
// app/src/tests/unit/libs/metadataUtils.test.ts: inside describe('getDateRange'), after the THROUGH_2036 test
    it('given metadata that ends in 2035 then the range ends in 2035, not the fallback', () => {
      // Given
      const state = mockStateWithTimePeriods(TEST_TIME_PERIOD_YEARS.US_THROUGH_2035) as RootState;

      // When
      const result = getDateRange(state);

      // Then
      expect(result).toEqual(EXPECTED_DATE_RANGES.US_THROUGH_2035);
    });

    it('given metadata from 2024 to 2030 then the range starts in 2024 and ends in 2030', () => {
      // Given
      const state = mockStateWithTimePeriods(TEST_TIME_PERIOD_YEARS.UK_2024_TO_2030) as RootState;

      // When
      const result = getDateRange(state);

      // Then
      expect(result).toEqual(EXPECTED_DATE_RANGES.UK_2024_TO_2030);
    });

Suggestions

G1. Test the store as it is while metadata loads

  • Where: app/src/tests/unit/libs/metadataUtils.test.ts:292-302.
  • Evidence: the test "given no time periods yet" uses a fully loaded store (loading: false, parameters, version 1.0.0) with time_period: []. Before load, the app has the reducer's pending state (app/src/reducers/metadataReducer.ts:74-80). The current test is valid for the selector's input, so this is about fidelity, not a bug.
  • Fix (passes; keep the existing NONE test, because it covers "loaded, with an empty year list"). The repo builds this state the same way in app/src/tests/unit/pages/reportBuilder/useReportSPMSelectionError.test.tsx:114, and testing.md:182 asks for test data in fixtures:
// metadataUtilsMocks.ts: imports
import metadataReducer, { fetchMetadataThunk } from '@/reducers/metadataReducer';

// metadataUtilsMocks.ts: at the end
// Helper to create the RootState that the app has while the metadata request is in flight
export const mockStateWhileMetadataLoads = (): Partial<RootState> => ({
  metadata: metadataReducer(undefined, fetchMetadataThunk.pending('request', 'us')),
});
// metadataUtils.test.ts: add mockStateWhileMetadataLoads to the fixture imports
    it('given the metadata request is still in flight then falls back to 2022 through 2036', () => {
      // Given
      const state = mockStateWhileMetadataLoads() as RootState;

      // When
      const result = getDateRange(state);

      // Then
      expect(result).toEqual(EXPECTED_DATE_RANGES.FALLBACK);
    });

G2. Say that the fallback is shared with the UK

  • Where: app/src/libs/metadataUtils.ts:34-40; PR body, Summary.
  • Evidence: getDateRange takes no country, and the UK metadata is 2024–2030. The shared fallback was already wrong for the UK at both ends on main. Users see no effect, but a reader of "US 2036 extension" can conclude that the UK now offers 2036.
  • Fix: add a comment above the if (Prettier, ESLint and tsc stay clean):
    // Applies only until the country's metadata loads, and is shared by the US
    // and the UK. Once loaded, the metadata's own years replace it.

Add to the Summary: "The fallback is shared with the UK and is not shown in today's UI: the date controls that read it render only after metadata loads."

G3. Refresh two stale statements in the PR body

  • Test count. The Test plan says "26 passed". The file now runs 40. The count was correct when written: commit 260dd810 (2026-10-22) added 14 search-visibility tests to the file before the rebase. Write: "Vitest, src/tests/unit/libs/metadataUtils.test.ts: 40 passed (43 with the added getDateRange cases)."
  • Merge order. The Summary says "so this PR can merge first". The merge hold was agreed on 2026-10-15, one day later, and PolicyEngine/policyengine-api#3926 lists this PR as "ready but held". Write: "The fallback applies only until metadata arrives, so merging this before #3928 changes nothing that users see. It is held under the merge hold in #3926 until spm-calculator 1.1.0 is on PyPI and 2036 runs end to end."

Investigated and cleared

  • Can the fallback let a user pick a 2036 that the API cannot run? No. Each caller renders only after metadata loads: DraftTable and ProvisionDetails wait for a non-empty parameter index (app/src/pages/flagship/Build.page.tsx:45), and the other two need a parameter from metadata. Parameters and time_period arrive in one reducer action (app/src/reducers/metadataReducer.ts:99-103). No production path displays the fallback.
  • Does getDateRange set the simulation year? No. The report-year picker reads getTaxYears (app/src/components/flagship/reform/DraftHeader.tsx:285-289).
  • Other stale 2035/2036 values. None need a change. app/src/pathways/report/components/policyParameterSelector/HistoricalValues.tsx:217 '2036-12-31' is a chart-axis fallback that is already on main.
  • Time zones and inclusive end. Plain YYYY-MM-DD strings, parsed by dayjs as local time; the date picker rejects only days after maxDate.

Merge timing

Hold the merge under the agreed 2036 hold until spm-calculator 1.1.0 is on PyPI and 2036 runs end to end. Today PyPI ends at 1.0.1, spm-calculator#51 has changes requested, and policyengine-api#3928 is a draft. Any exception needs explicit agreement from @anth-volk and @PavelMakarchuk on #3926. An early merge would not change what users see. The US metadata ends in 2035, loaded metadata replaces the fallback, and no production path shows a date control from the fallback. This PR alone also does not make the app offer 2036; only policyengine-api#3928 does. The app stops at 2035 because the API's years stop at 2035, not because this PR is unmerged. If the 2036 extension is dropped, revert the fallback to 2035-12-31.

Validation Summary

Check Result
CI at head 10/10 pass (Tests, Type checking, Lint and format, Build, Visual regression, Storybook, Vercel x2, embedded URLs)
metadataUtils.test.ts at head 40 passed
app/src/tests/unit/libs/ at head 69 files, 1171 passed
Whole app/ suite at head 457 files: 5901 passed, 10 skipped, 0 failed
With SA1, G1 and G2 applied Changed file 43 passed; whole suite 5904 passed
Mutation, PR tests (8 mutants) 6 killed by the changed file. "Always return the fallback" survives the file (caught only by DraftTable.test.tsx:170). "Metadata start fixed at 2022" survives the whole suite
Mutation, with fixes 8 of 8 killed by the changed file
Prettier, ESLint (--max-warnings 0), tsc --noEmit Clean at head and with fixes

Review Severity: REQUEST_CHANGES

One should-address item is open: the new tests do not prove that loaded metadata replaces the fallback.

Next Steps

  1. Add the two metadata cases from SA1 and run bun run vitest -- src/tests/unit/libs/metadataUtils.test.ts.
  2. Optionally add G1 (in-flight store test) and G2 (comment).
  3. Update the PR body (G2 sentence, G3 test count and merge order).
  4. Keep the PR unmerged until spm-calculator 1.1.0 is on PyPI and 2036 runs end to end, or until @anth-volk and @PavelMakarchuk agree an exception on policyengine-api#3926.
  5. Request round 2 after the push.

The existing cases could not tell metadata from the fallback, because
both gave 2022 to 2036. New cases use today's US (2022 to 2035) and UK
(2024 to 2030) years, so a selector that always returned the fallback,
or fixed the lower bound at 2022, now fails. A case for the store while
the metadata request is in flight covers the state the app actually has
before load. A comment says the fallback is shared by the US and the UK.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@DTrim99

DTrim99 commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

@PavelMakarchuk round 1 is addressed in 1b64c79:

  • SA1: I added your two metadata cases: today's US years (2022–2035) and the UK's (2024–2030). An always-fallback selector and a fixed 2022 lower bound now both fail.
  • G1: a case for the store while the metadata request is in flight (the reducer's pending state). The loaded-but-empty case stays too.
  • G2: a comment saying the fallback is shared by the US and the UK and applies only until metadata loads, plus your sentence in the description.
  • G3: the description now gives 43 tests and the merge order under the #3926 hold.

metadataUtils.test.ts passes 43. Prettier, ESLint (--max-warnings 0) and tsc are clean on the changed files. Ready for round 2.

@PavelMakarchuk PavelMakarchuk left a comment

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.

Code Review: PR #1266 "Fall back to a 2022–2036 date range before metadata loads" (@DTrim99, round 2)

ID legend: C = critical (must fix), SA = should address, G = suggestion.

Source Documents

  • Reviewed head SHA: 1b64c79
  • Mode: incremental (round 2), 7e9e173c71..1b64c79d9e
  • Scope: one commit, 1b64c79d9 "Test that loaded metadata replaces the date-range fallback": a 2-line comment in app/src/libs/metadataUtils.ts, 3 new getDateRange cases in app/src/tests/unit/libs/metadataUtils.test.ts, and new constants and a mockStateWhileMetadataLoads helper in app/src/tests/fixtures/libs/metadataUtilsMocks.ts. The PR body is updated.
  • Read: the incremental diff; getDateRange at the head; the fetchMetadataThunk.pending case in app/src/reducers/metadataReducer.ts:74-81; the PR body; PyPI spm-calculator; PolicyEngine/policyengine-api#3926 and #3928; spm-calculator#51.

Branch Status

  • 2 commits ahead of main and 63 behind. None of the 63 commits touch the three changed files or the metadata reducer, and git merge-tree is clean. GitHub reports the PR as blocked only by the required review.
  • CI at the head: 10/10 pass, including Type checking, so the new fetchMetadataThunk.pending('request', 'us') call typechecks in the full project.

Round 1 items

ID Status Evidence
SA1 RESOLVED The two metadata cases are in metadataUtils.test.ts:293-313 (the in-flight case is at :326), with US_THROUGH_2035 and UK_2024_TO_2030 in the fixture. I re-ran both round-1 mutants against the changed file. "Always return the fallback" now fails 2 tests (the 2035 and the 2024–2030 cases). "Metadata start fixed at 2022" now fails the 2024–2030 case. In round 1 the first survived the file and the second survived the whole suite.
G1 RESOLVED mockStateWhileMetadataLoads builds the store from the real reducer: metadataReducer(undefined, fetchMetadataThunk.pending('request', 'us')). The pending case returns initialState with loading: true (metadataReducer.ts:74-81), so the test pins the in-flight state that users would see, not a hand-written copy of it. The empty-list case stays.
G2 RESOLVED The comment is at metadataUtils.ts:35-36, and the Summary has the UK sentence.
G3 RESOLVED The Test plan gives 43 tests (40 after the rebase, plus 3 new cases), which matches my run. The Summary states the merge hold under PolicyEngine/policyengine-api#3926.

New findings

Critical

None.

Should address

None.

Suggestions

None.

Tests run

Check Result
metadataUtils.test.ts at the head 43 passed
Mutant "always return the fallback" killed (2 failures)
Mutant "metadata start fixed at 2022" killed (1 failure)
CI at the head 10/10 pass

Merge timing

The hold from round 1 still applies. Today PyPI spm-calculator ends at 1.0.1, and PolicyEngine/policyengine-api#3928 is still a draft. spm-calculator#51 is approved but not merged or released. Keep this PR unmerged until spm-calculator 1.1.0 is on PyPI and 2036 runs end to end, or until @anth-volk and @PavelMakarchuk agree an exception on #3926. As round 1 found, an early merge would not change what users see.

Review Severity: APPROVE

All round-1 items are resolved, and the new tests now prove that loaded metadata replaces the fallback. Merge only after the 2036 hold lifts.

This branch was successfully deployed

2 active deployments
Preview – policyengine-website — 1b64c79d Deployed Oct 27, 2026 by vercel[bot]
Preview – policyengine-calculator-next — 1b64c79d Deployed Oct 27, 2026 by vercel[bot]
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