From 230d17c3d268773a59211402b103069bcb9f15a7 Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Fri, 6 Nov 2026 13:21:31 -0500 Subject: [PATCH 1/2] Calculate 2036 on the 2.2.x line with spm-calculator 1.1 Require spm-calculator 1.1, whose canonical forecast covers 2022-2036, and add CBO's February 2026 calendar-2036 CPI-U and chained CPI-U points, projecting both through 2036 so their authored 2035 points are no longer overwritten. Single-year datasets now extend through 2036 by default and IRS uprating projects through tax year 2036. This is the minimal CPI-U subset of #10227 plus the #10620 pin, for a 2.2.2 patch inside the current US data release's certified range. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/release-lock.md | 6 +++--- changelog.d/cbo-cpi-2036.changed.md | 1 + changelog.d/spm-calculator-1-1.changed.md | 1 + docs/spm.md | 2 +- .../parameters/gov/bls/cpi/c_cpi_u.yaml | 3 ++- policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml | 3 ++- policyengine_us/parameters/uprating_extensions.py | 15 ++++++++++----- .../data/test_extend_single_year_dataset.py | 6 +++--- .../spm_unit/test_spm_unit_spm_threshold.py | 7 ++++--- pyproject.toml | 2 +- uv.lock | 8 ++++---- 11 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 changelog.d/cbo-cpi-2036.changed.md create mode 100644 changelog.d/spm-calculator-1-1.changed.md diff --git a/.github/release-lock.md b/.github/release-lock.md index 7a2db0ebf99..980e1375fce 100644 --- a/.github/release-lock.md +++ b/.github/release-lock.md @@ -75,11 +75,11 @@ The opt-in probe creates an isolated, minimal project with a standard PyPI dependency and exercises actual uv locking. CI enables it. These commands do not install or import the country model. -For the SPM integration, `spm-calculator` 1.0.0 reached PyPI on 2026-09-11, and +For the SPM integration, `spm-calculator` 1.1.0 reached PyPI on 2026-11-02, and the committed country lock resolves it from `https://pypi.org/simple` with sdist and wheel SHA-256 hashes, so `--committed` passes on the committed files. -`pyproject.toml` requires `>=1.0.0,<=1.0.0.post1` and the lock pins 1.0.0; -moving that pin to the post-release is an ordinary reviewed lock change. This +`pyproject.toml` requires `>=1.1.0,<1.2` and the lock pins 1.1.0; moving to a +later 1.1.x release is an ordinary reviewed lock change. This guard validates dependency sources, artifact URLs and hashes, never a version string, so the calculator requirement is held by `pyproject.toml` and review rather than by `release_lock.py`. A stale root version or an older calculator diff --git a/changelog.d/cbo-cpi-2036.changed.md b/changelog.d/cbo-cpi-2036.changed.md new file mode 100644 index 00000000000..ae5378e7d70 --- /dev/null +++ b/changelog.d/cbo-cpi-2036.changed.md @@ -0,0 +1 @@ +- Add CBO's February 2026 calendar-2036 CPI-U and chained CPI-U points and keep their authored 2035 points instead of extrapolating over them, so single-year datasets extend through 2036 by default and IRS uprating projects through tax year 2036. diff --git a/changelog.d/spm-calculator-1-1.changed.md b/changelog.d/spm-calculator-1-1.changed.md new file mode 100644 index 00000000000..9b11a8dc2f3 --- /dev/null +++ b/changelog.d/spm-calculator-1-1.changed.md @@ -0,0 +1 @@ +- Require spm-calculator 1.1, whose canonical forecast covers 2022–2036, so SPM thresholds and SPM resources calculate for 2036. diff --git a/docs/spm.md b/docs/spm.md index bc5fbe9b5a9..ea182d20972 100644 --- a/docs/spm.md +++ b/docs/spm.md @@ -3,7 +3,7 @@ The country model uses the installed `spm-calculator` artifact for all five SPM measurement amounts. It does not extrapolate thresholds with country CPI or income parameters. Each supported year and scenario comes from the verified artifact. -The installed 1.0.0 artifact covers 2022 through 2035; unavailable years fail. +The installed 1.1.0 artifact covers 2022 through 2036; unavailable years fail. Taxes, benefits, resources and the housing-assistance cap remain country-model formulas. General household benefits and CBO transfer aggregates count actual `housing_assistance`; only SPM resources count diff --git a/policyengine_us/parameters/gov/bls/cpi/c_cpi_u.yaml b/policyengine_us/parameters/gov/bls/cpi/c_cpi_u.yaml index f9f7cc3021e..851b5f35b38 100644 --- a/policyengine_us/parameters/gov/bls/cpi/c_cpi_u.yaml +++ b/policyengine_us/parameters/gov/bls/cpi/c_cpi_u.yaml @@ -332,7 +332,8 @@ values: 2033-02-01: 210.8 # 2034 value. 2034-02-01: 214.9 # 2035 value. 2035-02-01: 219.2 # 2036 value. - # Values for 2036-2100 are set programmatically in uprating_extensions.py + 2036-02-01: 223.5 # 2037 value. + # Values for 2037-2100 are set programmatically in uprating_extensions.py metadata: economy: false unit: currency-USD diff --git a/policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml b/policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml index 2a76d76e7ec..94d670cb424 100644 --- a/policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml +++ b/policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml @@ -1373,7 +1373,8 @@ values: 2033-02-01: 389.6 # 2034 value. 2034-02-01: 398.4 # 2035 value. 2035-02-01: 407.3 # 2036 value. - # Values for 2036-2100 are set programmatically in uprating_extensions.py + 2036-02-01: 416.5 # 2037 value. + # Values for 2037-2100 are set programmatically in uprating_extensions.py metadata: unit: currency-USD label: CPI-U diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index c186d2d212a..f57c31e2bd1 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -373,7 +373,9 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: # IRS uprating - special case that computes from CPI IRS_UPRATING_START_YEAR = 2010 - IRS_LAST_PROJECTED_YEAR = 2035 + # 2036 averages chained CPI-U from August 2034 to July 2035, the 2034 and + # 2035 February instants, which hold CBO's February 2026 calendar points. + IRS_LAST_PROJECTED_YEAR = 2036 uprating_index = parameters.gov.irs.uprating @@ -446,19 +448,22 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: period_day=1, ) - # CPI-U (February values, last projection year 2034) + # CPI-U (February values, last projection year 2036). Each February + # instant holds CBO's February 2026 calendar-year average for that year + # (the "# YYYY value" comments name the tax year it feeds), so CBO's last + # point, calendar 2036, sits at the 2036 instant. extend_parameter_values( parameters.gov.bls.cpi.cpi_u, - last_projected_year=2034, + last_projected_year=2036, end_year=END_YEAR, period_month=2, period_day=1, ) - # Chained CPI-U (February values, last projection year 2034) + # Chained CPI-U (February values, last projection year 2036), as for CPI-U. extend_parameter_values( parameters.gov.bls.cpi.c_cpi_u, - last_projected_year=2034, + last_projected_year=2036, end_year=END_YEAR, period_month=2, period_day=1, diff --git a/policyengine_us/tests/microsimulation/data/test_extend_single_year_dataset.py b/policyengine_us/tests/microsimulation/data/test_extend_single_year_dataset.py index 1b3ccad7e01..cc4b0a3fd32 100644 --- a/policyengine_us/tests/microsimulation/data/test_extend_single_year_dataset.py +++ b/policyengine_us/tests/microsimulation/data/test_extend_single_year_dataset.py @@ -1103,14 +1103,14 @@ class FakeParam: def test_with_real_cpi_u_parameter(self): """Verify get_parameter_last_year works on the actual CPI-U - parameter YAML, returning 2035 (not 2100 from uprating_extensions).""" + parameter YAML, returning 2036 (not 2100 from uprating_extensions).""" from policyengine_us.system import system cpi_u = _resolve_parameter(system.parameters, "gov.bls.cpi.cpi_u") last_year = get_parameter_last_year(cpi_u) - # CPI-U YAML currently extends to 2035; if the YAML is updated, + # CPI-U YAML currently extends to 2036; if the YAML is updated, # this assertion should be updated to match. - assert last_year == 2035 + assert last_year == 2036 # --------------------------------------------------------------------------- diff --git a/policyengine_us/tests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py b/policyengine_us/tests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py index 4849d9f7086..a61f7e95669 100644 --- a/policyengine_us/tests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py +++ b/policyengine_us/tests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py @@ -74,7 +74,7 @@ def canonical(forecast, year, tenure, *, adults=2, children=2, county=None): ) def test_every_amount_uses_canonical_final_value_for_every_supported_year(tenure): forecast = load_forecast() - assert list(forecast.years) == list(range(2022, 2036)) + assert list(forecast.years) == list(range(2022, 2037)) # Explicit period inputs cover the artifact, including its historical years. situation = household(county="06037", tenure=tenure) for person in situation["people"].values(): @@ -164,10 +164,11 @@ def test_unavailable_selection_or_consumer_extrapolation_rejected(config): create_spm_provider(config) -def test_unknown_year_has_no_country_cpi_extrapolation(): +@pytest.mark.parametrize("year", [2037, 2099]) +def test_unknown_year_has_no_country_cpi_extrapolation(year): simulation = Simulation(situation=household(county="06037")) with pytest.raises(ValueError, match="no entry"): - simulation.calculate("spm_unit_spm_threshold", 2099) + simulation.calculate("spm_unit_spm_threshold", year) @pytest.mark.parametrize( diff --git a/pyproject.toml b/pyproject.toml index 6efb89cebd5..ff3149dd48f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ # extension and is available on every supported Python version. "pandas>=3.0", "policyengine-core>=3.30.1", - "spm-calculator>=1.0.0,<=1.0.0.post1", + "spm-calculator>=1.1.0,<1.2", "tables>=3.9", "tqdm>=4.67.1", ] diff --git a/uv.lock b/uv.lock index 2cd6cabc140..d25c6e4b5d4 100644 --- a/uv.lock +++ b/uv.lock @@ -1763,7 +1763,7 @@ requires-dist = [ { name = "policyengine-core", specifier = ">=3.30.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9.0" }, { name = "setuptools", marker = "extra == 'dev'", specifier = ">=80.9.0" }, - { name = "spm-calculator", specifier = ">=1.0.0,<=1.0.0.post1" }, + { name = "spm-calculator", specifier = ">=1.1.0,<1.2" }, { name = "tables", specifier = ">=3.9" }, { name = "towncrier", marker = "extra == 'dev'", specifier = ">=24.8.0" }, { name = "tqdm", specifier = ">=4.67.1" }, @@ -2443,7 +2443,7 @@ wheels = [ [[package]] name = "spm-calculator" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "census" }, @@ -2453,9 +2453,9 @@ dependencies = [ { name = "requests" }, { name = "us" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/5e/3f28b212c401795990250bb3daec741f699a8c7dc2a23a3db7005df6feb6/spm_calculator-1.0.0.tar.gz", hash = "sha256:a99aac8c2c0bf81a9455105bbf872366f1ea9066a99cd793714881cd3db846dc", size = 7150825, upload-time = "2026-09-11T15:45:12.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/84/19057fa04e6c8cce85494458b2164d473ce82cbf54608a3b2951af2c36b1/spm_calculator-1.1.0.tar.gz", hash = "sha256:1be418c6a1baaba86f63c3287b38efecf7f94af563b864a73934ecbf60a01f38", size = 9625137, upload-time = "2026-11-02T18:48:45.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/a0/c484f69a0ebf88a9b9fd0ac28176f8df66550f46714f1b0c5ec0b360824e/spm_calculator-1.0.0-py3-none-any.whl", hash = "sha256:e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305", size = 7395384, upload-time = "2026-09-11T15:45:10.492Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/12a744eb7f3445db8899dc187bbd193c884f9fe4c373851c0ddb24cc0f5e/spm_calculator-1.1.0-py3-none-any.whl", hash = "sha256:c528ec0f06eeb1188b7d6cf456ff3a9b73a0fd8856b3938358251cc5f9be83e8", size = 9881340, upload-time = "2026-11-02T18:48:42.665Z" }, ] [[package]] From d2e541169029ece444ed97399c36a7097fc87560 Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Mon, 9 Nov 2026 11:51:06 -0500 Subject: [PATCH 2/2] Run the PR and release workflows on release/2.2.x Let pull requests into release/2.2.x run the PR suite, and let pushes to it version and publish. Publish now uses PyPI trusted publishing in the pypi environment, as main does, because the stored PyPI token this workflow read is gone. The pypi environment's deployment branches must include release/2.2.x before the release push. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/pr.yaml | 2 +- .github/workflows/push.yaml | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 3e3a91d52d2..66469601efe 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -1,7 +1,7 @@ name: Pull request on: pull_request: - branches: [main] + branches: [main, release/2.2.x] # Concurrency: auto-cancels previous runs when new commits are pushed concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 62636db7d47..8f1de9a0e29 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -1,7 +1,7 @@ name: Push on: push: - branches: [main] + branches: [main, release/2.2.x] jobs: ReleaseLock: @@ -333,6 +333,13 @@ jobs: (github.repository == 'PolicyEngine/policyengine-us') && (github.event.head_commit.message == 'Update PolicyEngine US') needs: [ReleaseLock, ReleaseLockTests, Baseline, HouseholdAPIPartners, Contrib, Rest, Microsimulation] + # PyPI trusted publishing, as on main: PyPI accepts uploads only from this + # workflow in the pypi environment, whose deployment branches must include + # release/2.2.x. No stored PyPI token. + environment: pypi + permissions: + contents: read + id-token: write steps: - name: Checkout repo uses: actions/checkout@v6 @@ -355,6 +362,4 @@ jobs: - name: Publish a Python distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: ${{ secrets.PYPI }} skip-existing: true