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/.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 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]]