From 6c0f20e1df6e7672df8a913295f6f4e2613e9c53 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 14:23:58 -0400 Subject: [PATCH 1/4] Index Medicaid LTC home equity limits on January 1 and label the 2027 projection The agricultural home equity limit used plain CPI-U uprating, which follows the CPI-U series' February 1 projection keys, so the limit stepped on February 1 (2028: $1,189,000 in January, $1,217,000 from February). It now uses the September-to-September CPI-U cadence effective January 1 that CMS applies under 42 USC 1396p(f)(1)(C), which reproduces CMS's published 2026 limits from 2025. Its unpublished 2027 and 2028 values are dropped in favor of that uprating. The non-agricultural limit's 2027 value ($1,159,000) appears in no CMS bulletin; it is the CPI-U projection. Uprating cannot fill a year before the fixed 2028 $1,000,000 value, so it stays, labeled as a projection, with a test that keeps it equal to the uprated agricultural limit. The bulletin citations now carry their actual subject lines and pages. Co-Authored-By: Claude Opus 5.5 --- .../fix-home-equity-limit-projection.fixed.md | 1 + .../home_equity/agricultural_limit.yaml | 25 +++++---- .../long_term_care/home_equity/limit.yaml | 19 ++++--- ...est_medicaid_home_equity_limit_indexing.py | 53 +++++++++++++++++++ ...d_long_term_care_home_equity_eligible.yaml | 14 ++--- 5 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 changelog.d/fix-home-equity-limit-projection.fixed.md create mode 100644 policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py diff --git a/changelog.d/fix-home-equity-limit-projection.fixed.md b/changelog.d/fix-home-equity-limit-projection.fixed.md new file mode 100644 index 00000000000..cb542cb985e --- /dev/null +++ b/changelog.d/fix-home-equity-limit-projection.fixed.md @@ -0,0 +1 @@ +Index the Medicaid long-term care agricultural home equity limit each January 1 instead of each February 1, drop its unpublished 2027 and 2028 values, label the 2027 non-agricultural limit as a CPI-U projection, and correct the CMS bulletin citations. diff --git a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml index d9995ce6432..7eb2224c853 100644 --- a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml +++ b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml @@ -2,26 +2,33 @@ description: The Department of Health and Human Services limits agricultural hom values: 2025-01-01: 1_097_000 2026-01-01: 1_130_000 - 2027-01-01: 1_159_000 - 2028-01-01: 1_189_000 metadata: unit: currency-USD period: year label: Medicaid long-term care agricultural home equity limit + # 42 USC 1396p(f)(1)(C) increases the limit from year to year by CPI-U, + # rounded to the nearest $1,000. CMS applies the September-to-September + # change each January 1, which reproduces the published 2026 value from + # 2025. P.L. 119-21 section 71108 exempts this limit from the $1,000,000 + # cap that applies to other homes from 2028. uprating: parameter: gov.bls.cpi.cpi_u + at_defined_interval: + start: "0001-09-01" + end: "0002-09-01" + enactment: "0003-01-01" rounding: type: nearest interval: 1_000 reference: - - title: Updated 2025 SSI and Spousal Impoverishment Standards - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf - - title: 2026 SSI, Spousal Impoverishment, and Medicare Savings Program Resource Standards - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf - - title: CMCS Informational Bulletin, Dec. 9, 2025 + - title: Updated 2025 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, May 28, 2025 + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf#page=2 + - title: Updated 2026 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, April 27, 2026 + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf#page=2 + - title: 2026 SSI, Spousal Impoverishment, and Medicare Savings Program Resource Standards, CMCS Informational Bulletin, December 9, 2025 href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib12092025.pdf#page=3 - - title: CMCS Informational Bulletin, Nov. 18, 2025 - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib11182025.pdf#page=8 + - title: CMCS Informational Bulletin, November 18, 2025, Section 71108 - Revising Home Equity Limit + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib11182025.pdf#page=9 - title: H.R. 1, 119th Congress, Sec. 71108 - Revising home equity limit for determining eligibility for long-term care services under the Medicaid program href: https://www.congress.gov/bill/119th-congress/house-bill/1/text diff --git a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml index 1e657c53b4e..92ec725c6a3 100644 --- a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml +++ b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml @@ -2,6 +2,11 @@ description: The Department of Health and Human Services limits non-agricultural values: 2025-01-01: 1_097_000 2026-01-01: 1_130_000 + # CMS has not published 2027 standards. This is the 2026 value indexed by + # the September 2025 to September 2026 change in CPI-U, rounded to the + # nearest $1,000, the method that reproduces CMS's 2026 value from 2025. + # It must equal the uprated 2027 agricultural_limit; replace it with the + # CMS figure when the 2027 bulletin is released. 2027-01-01: 1_159_000 2028-01-01: 1_000_000 @@ -10,11 +15,13 @@ metadata: period: year label: Medicaid long-term care non-agricultural home equity limit reference: - - title: Updated 2025 SSI and Spousal Impoverishment Standards - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf - - title: 2026 SSI, Spousal Impoverishment, and Medicare Savings Program Resource Standards - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf + - title: Updated 2025 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, May 28, 2025 + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf#page=2 + - title: Updated 2026 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, April 27, 2026 + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf#page=2 + - title: 42 USC 1396p(f)(1)(C) - Annual CPI-U indexing of the home equity limits (2027 projection) + href: https://www.law.cornell.edu/uscode/text/42/1396p#f_1 - title: H.R. 1, 119th Congress, Sec. 71108 - Revising home equity limit for determining eligibility for long-term care services under the Medicaid program href: https://www.congress.gov/bill/119th-congress/house-bill/1/text - - title: CMCS Informational Bulletin, Nov. 18, 2025 - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib11182025.pdf#page=8 + - title: CMCS Informational Bulletin, November 18, 2025, Section 71108 - Revising Home Equity Limit + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib11182025.pdf#page=9 diff --git a/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py new file mode 100644 index 00000000000..7fc6a2fa5ed --- /dev/null +++ b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py @@ -0,0 +1,53 @@ +"""Indexing of the Medicaid long-term care home equity limits. + +42 USC 1396p(f)(1)(C) increases the limits from year to year by CPI-U, +rounded to the nearest $1,000. CMS applies the September-to-September change +each January 1. These tests check that the parameters follow that method and +that the hand-entered 2027 non-agricultural projection agrees with the +uprated agricultural limit, which is the same indexed amount before P.L. +119-21 section 71108 caps non-agricultural homes at $1,000,000 in 2028. +""" + +import pytest + +from policyengine_us.system import system + +HOME_EQUITY = system.parameters.gov.hhs.medicaid.eligibility.long_term_care.home_equity +CPI_U = system.parameters.gov.bls.cpi.cpi_u + + +def indexed(value, year): + change = CPI_U(f"{year - 1}-09-01") / CPI_U(f"{year - 2}-09-01") + return round(value * change / 1_000) * 1_000 + + +@pytest.mark.parametrize("name", ["limit", "agricultural_limit"]) +def test_cms_method_reproduces_the_published_2026_limit(name): + parameter = getattr(HOME_EQUITY, name) + # CMS published $1,097,000 for 2025 and $1,130,000 for 2026. + assert indexed(parameter("2025-01-01"), 2026) == parameter("2026-01-01") + + +def test_agricultural_limit_steps_only_on_january_1(): + for year in range(2027, 2041): + prior = HOME_EQUITY.agricultural_limit(f"{year - 1}-01-01") + current = HOME_EQUITY.agricultural_limit(f"{year}-01-01") + assert current == indexed(prior, year), year + for month in ["02", "07", "12"]: + assert HOME_EQUITY.agricultural_limit(f"{year}-{month}-01") == current + + +def test_2027_non_agricultural_projection_matches_the_uprated_limit(): + projected = HOME_EQUITY.limit("2027-01-01") + uprated = HOME_EQUITY.agricultural_limit("2027-01-01") + assert projected == uprated, ( + "home_equity/limit.yaml's 2027 value is a CPI-U projection; set it to " + f"{uprated:,.0f}, the uprated agricultural limit, or to the CMS figure " + "once published (and add that figure to agricultural_limit.yaml too)." + ) + + +def test_non_agricultural_cap_holds_from_2028(): + for year in range(2028, 2041): + assert HOME_EQUITY.limit(f"{year}-01-01") == 1_000_000 + assert HOME_EQUITY.agricultural_limit(f"{year}-01-01") > 1_000_000 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml index aeb4936abee..de1ebdc56e0 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml @@ -22,11 +22,11 @@ output: is_medicaid_long_term_care_home_equity_eligible: false -- name: Case 3b, current-law home equity limit uprates in 2027. +- name: Case 3b, the home equity limit is indexed above its 2026 value in 2027. period: 2027 input: receives_medicaid_long_term_care_services: true - home_equity: 1_159_000 + home_equity: 1_130_001 output: is_medicaid_long_term_care_home_equity_eligible: true @@ -38,12 +38,12 @@ output: is_medicaid_long_term_care_home_equity_eligible: true -- name: Case 5, agricultural home at the agricultural home equity limit remains eligible. +- name: Case 5, agricultural home above the non-agricultural cap remains eligible. period: 2028 input: receives_medicaid_long_term_care_services: true home_is_on_agricultural_land: true - home_equity: 1_189_000 + home_equity: 1_000_001 output: is_medicaid_long_term_care_home_equity_eligible: true @@ -103,7 +103,7 @@ input: receives_medicaid_long_term_care_services: true home_is_on_agricultural_land: true - home_equity: 1_189_001 + home_equity: 2_000_000 output: is_medicaid_long_term_care_home_equity_eligible: false @@ -283,7 +283,7 @@ input: receives_medicaid_long_term_care_services: true home_is_on_agricultural_land: true - home_equity: 1_217_000 + home_equity: 1_130_001 output: is_medicaid_long_term_care_home_equity_eligible: true @@ -292,7 +292,7 @@ input: receives_medicaid_long_term_care_services: true home_is_on_agricultural_land: true - home_equity: 1_217_001 + home_equity: 2_000_000 output: is_medicaid_long_term_care_home_equity_eligible: false From 6a54ffeb56c0342fe3510ea1e533d6e8848691b1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 14:59:01 -0400 Subject: [PATCH 2/4] Project home equity limits with CMS's rounded CPI-U increase Review r1 found that the Sep-to-Sep CPI-U ratio misses CMS's published 2021 maximum: CMS applies its reported "CPI Increase" rounded to 0.1% ($893,000 x 1.014 = $906,000; the unrounded 1.371% gives $905,000). The model's CPI-U series is also seasonally adjusted from 2024, so it gives 2.3% for 2025 where CMS used 2.4%. extend_medicaid_home_equity_limits now projects the limits in uprating_extensions.py, following the Louisiana precedent: the percentage is the not seasonally adjusted September-to-September change (published Septembers in the new cpi_u_nsa_september, otherwise both months from the model series), rounded to 0.1% and floored at zero, applied year to year and rounded to the nearest $1,000. This reproduces every CMS percentage on file (2020, 2021, 2022, 2025, 2026) and the published 2021, 2022, 2025 and 2026 limits. The non-agricultural 2027 value is now derived rather than hand-entered ($1,165,000 on the current CPI-U path), and a YAML case with a parameter override restores agricultural boundary coverage after 2028. Co-Authored-By: Claude Opus 5.5 --- .../fix-home-equity-limit-projection.fixed.md | 2 +- .../gov/bls/cpi/cpi_u_nsa_september.yaml | 18 ++++ .../home_equity/agricultural_limit.yaml | 20 ++-- .../long_term_care/home_equity/limit.yaml | 11 +-- .../parameters/uprating_extensions.py | 93 +++++++++++++++++++ ...est_medicaid_home_equity_limit_indexing.py | 91 ++++++++++++------ ...d_long_term_care_home_equity_eligible.yaml | 28 +++++- 7 files changed, 212 insertions(+), 51 deletions(-) create mode 100644 policyengine_us/parameters/gov/bls/cpi/cpi_u_nsa_september.yaml diff --git a/changelog.d/fix-home-equity-limit-projection.fixed.md b/changelog.d/fix-home-equity-limit-projection.fixed.md index cb542cb985e..672ec8fab91 100644 --- a/changelog.d/fix-home-equity-limit-projection.fixed.md +++ b/changelog.d/fix-home-equity-limit-projection.fixed.md @@ -1 +1 @@ -Index the Medicaid long-term care agricultural home equity limit each January 1 instead of each February 1, drop its unpublished 2027 and 2028 values, label the 2027 non-agricultural limit as a CPI-U projection, and correct the CMS bulletin citations. +Project the Medicaid long-term care home equity limits with CMS's method (the September-to-September CPI-U increase rounded to 0.1%, effective each January 1) instead of stepping the agricultural limit each February 1, drop their unpublished 2027 and 2028 values, and correct the CMS bulletin citations. diff --git a/policyengine_us/parameters/gov/bls/cpi/cpi_u_nsa_september.yaml b/policyengine_us/parameters/gov/bls/cpi/cpi_u_nsa_september.yaml new file mode 100644 index 00000000000..738737b408f --- /dev/null +++ b/policyengine_us/parameters/gov/bls/cpi/cpi_u_nsa_september.yaml @@ -0,0 +1,18 @@ +description: The Bureau of Labor Statistics publishes this September value of the Consumer Price Index for All Urban Consumers, not seasonally adjusted, from which CMS takes the September-to-September percentage change it applies to the Medicaid long-term care home equity limits. +values: + # BLS series CUUR0000SA0 (U.S. city average, all items, 1982-84=100). + 2018-09-01: 252.439 + 2019-09-01: 256.759 + 2020-09-01: 260.28 + 2021-09-01: 274.31 + 2022-09-01: 296.808 + 2023-09-01: 307.789 + 2024-09-01: 315.301 + 2025-09-01: 324.8 +metadata: + unit: currency-USD + label: CPI-U, not seasonally adjusted, September + economy: false + reference: + - title: BLS | CPI-U, U.S. city average, all items, not seasonally adjusted (CUUR0000SA0) + href: https://data.bls.gov/timeseries/CUUR0000SA0 diff --git a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml index 7eb2224c853..f003e8e9355 100644 --- a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml +++ b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml @@ -7,20 +7,12 @@ metadata: unit: currency-USD period: year label: Medicaid long-term care agricultural home equity limit - # 42 USC 1396p(f)(1)(C) increases the limit from year to year by CPI-U, - # rounded to the nearest $1,000. CMS applies the September-to-September - # change each January 1, which reproduces the published 2026 value from - # 2025. P.L. 119-21 section 71108 exempts this limit from the $1,000,000 - # cap that applies to other homes from 2028. - uprating: - parameter: gov.bls.cpi.cpi_u - at_defined_interval: - start: "0001-09-01" - end: "0002-09-01" - enactment: "0003-01-01" - rounding: - type: nearest - interval: 1_000 + # Years after the last CMS-published value are projected in + # parameters/uprating_extensions.py (extend_medicaid_home_equity_limits) by + # CMS's method under 42 USC 1396p(f)(1)(C): the prior year's limit times the + # September-to-September CPI-U increase rounded to 0.1%, then rounded to the + # nearest $1,000, effective January 1. P.L. 119-21 section 71108 exempts this + # limit from the $1,000,000 cap on other homes from 2028. reference: - title: Updated 2025 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, May 28, 2025 href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf#page=2 diff --git a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml index 92ec725c6a3..118f0e15970 100644 --- a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml +++ b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml @@ -2,12 +2,9 @@ description: The Department of Health and Human Services limits non-agricultural values: 2025-01-01: 1_097_000 2026-01-01: 1_130_000 - # CMS has not published 2027 standards. This is the 2026 value indexed by - # the September 2025 to September 2026 change in CPI-U, rounded to the - # nearest $1,000, the method that reproduces CMS's 2026 value from 2025. - # It must equal the uprated 2027 agricultural_limit; replace it with the - # CMS figure when the 2027 bulletin is released. - 2027-01-01: 1_159_000 + # CMS has not published 2027 standards. Until then 2027 is projected in + # parameters/uprating_extensions.py (extend_medicaid_home_equity_limits) as + # the indexed maximum, which equals the agricultural limit before 2028. 2028-01-01: 1_000_000 metadata: @@ -19,7 +16,7 @@ metadata: href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf#page=2 - title: Updated 2026 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, April 27, 2026 href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf#page=2 - - title: 42 USC 1396p(f)(1)(C) - Annual CPI-U indexing of the home equity limits (2027 projection) + - title: 42 USC 1396p(f)(1)(C) - Annual CPI-U indexing of the home equity limits href: https://www.law.cornell.edu/uscode/text/42/1396p#f_1 - title: H.R. 1, 119th Congress, Sec. 71108 - Revising home equity limit for determining eligibility for long-term care services under the Medicaid program href: https://www.congress.gov/bill/119th-congress/house-bill/1/text diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 75d41b4268a..57c51e536e4 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -571,6 +571,96 @@ def extend_la_cpi_u_indexed_amounts(parameters: ParameterNode, end_year: int) -> ) +def get_cms_cpi_u_percentage_increase(cpi: ParameterNode, year: int) -> float: + """The CPI-U percentage increase CMS applies to ``year``'s home equity limits. + + 42 USC 1396p(f)(1)(C) increases the Medicaid long-term care home equity + limits "from year to year based on the percentage increase in the consumer + price index for all urban consumers ... rounded to the nearest $1,000". + CMS's annual SSI and spousal impoverishment bulletins report a "CPI + Increase" for each year to one decimal place: 1.7% for 2020, 1.4% for + 2021, 5.4% for 2022, 2.4% for 2025 and 3.0% for 2026. Each is the + September-to-September change in the not seasonally adjusted CPI-U ending + the September before the year, rounded to one decimal place. CMS applies + the rounded percentage: $893,000 x 1.014 = $905,502 became the published + 2021 maximum of $906,000, where the unrounded 1.371% would give $905,000. + A fall in the index is not an increase, so the percentage is never + negative. + + ``cpi`` is the ``gov.bls.cpi`` node. The published Septembers in + ``cpi_u_nsa_september`` are used when both are available. Otherwise both + Septembers come from the model's CPI-U series (seasonally adjusted from + January 2024) through ``get_projected_cpi_u_for_month``, so the two are + never mixed. + """ + september = cpi.cpi_u_nsa_september + published = {int(value.instant_str[:4]) for value in september.values_list} + if {year - 2, year - 1} <= published: + current = september(f"{year - 1}-09-01") + prior = september(f"{year - 2}-09-01") + else: + current = get_projected_cpi_u_for_month(cpi.cpi_u, instant(f"{year - 1}-09-01")) + prior = get_projected_cpi_u_for_month(cpi.cpi_u, instant(f"{year - 2}-09-01")) + change = 100 * (Decimal(repr(current)) / Decimal(repr(prior)) - 1) + percentage = change.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) + return float(max(percentage, 0)) + + +def adjust_home_equity_limit_for_inflation( + prior_amount: float, percentage: float +) -> float: + """Apply a 42 USC 1396p(f)(1)(C) increase, rounded to the nearest $1,000.""" + adjusted = Decimal(str(prior_amount)) * (100 + Decimal(str(percentage))) / 100 + thousands = (adjusted / 1_000).quantize(Decimal(1), rounding=ROUND_HALF_UP) + return float(thousands * 1_000) + + +def extend_medicaid_home_equity_limits( + parameters: ParameterNode, end_year: int +) -> None: + """Project the CPI-indexed Medicaid long-term care home equity limits. + + Each year after the last CMS-published value, the agricultural home + equity limit becomes the prior year's limit increased by that year's + CPI-U percentage (see ``get_cms_cpi_u_percentage_increase``), rounded to + the nearest $1,000, effective January 1. Before P.L. 119-21 section 71108 + caps other homes at $1,000,000 beginning in 2028, the agricultural and + non-agricultural limits are the same indexed maximum. So the + non-agricultural limit takes the agricultural projection for each year + between its last published value and its final value, the statutory + cap. Amounts encoded in the YAML take precedence for their own years. + + Like the other projections in this module, the values are fixed when the + parameter tree is built. + """ + home_equity = parameters.gov.hhs.medicaid.eligibility.long_term_care.home_equity + cpi = parameters.gov.bls.cpi + agricultural = home_equity.agricultural_limit + first_projected_year = 1 + max( + int(value.instant_str[:4]) for value in agricultural.values_list + ) + for year in range(first_projected_year, end_year + 1): + agricultural.update( + period=f"year:{year}-01-01:1", + value=adjust_home_equity_limit_for_inflation( + agricultural(f"{year - 1}-01-01"), + get_cms_cpi_u_percentage_increase(cpi, year), + ), + ) + agricultural.update( + start=instant(f"{end_year}-01-01"), + value=agricultural(f"{end_year}-01-01"), + ) + non_agricultural = home_equity.limit + years = sorted(int(value.instant_str[:4]) for value in non_agricultural.values_list) + cap_year, last_published_year = years[-1], years[-2] + for year in range(last_published_year + 1, cap_year): + non_agricultural.update( + period=f"year:{year}-01-01:1", + value=agricultural(f"{year}-01-01"), + ) + + def extend_wa_millionaires_standard_deduction( parameters: ParameterNode, end_year: int, @@ -1036,6 +1126,9 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: # above so projected Decembers are available. extend_la_cpi_u_indexed_amounts(parameters, end_year=END_YEAR) + # Medicaid long-term care home equity limits, CMS's rounded CPI-U method + extend_medicaid_home_equity_limits(parameters, end_year=END_YEAR) + # The limits on a dependent's standard deduction follow 26 U.S.C. # 63(c)(4)'s chained CPI-U schedule, computed from the statutory 1987 and # 1997 bases. Must run after the Chained CPI-U extension above so diff --git a/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py index 7fc6a2fa5ed..ffb24cf2ece 100644 --- a/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py +++ b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py @@ -1,53 +1,88 @@ -"""Indexing of the Medicaid long-term care home equity limits. +"""Projection of the Medicaid long-term care home equity limits. 42 USC 1396p(f)(1)(C) increases the limits from year to year by CPI-U, -rounded to the nearest $1,000. CMS applies the September-to-September change -each January 1. These tests check that the parameters follow that method and -that the hand-entered 2027 non-agricultural projection agrees with the -uprated agricultural limit, which is the same indexed amount before P.L. -119-21 section 71108 caps non-agricultural homes at $1,000,000 in 2028. +rounded to the nearest $1,000. ``extend_medicaid_home_equity_limits`` projects +them the way CMS computes them. These tests check that method against CMS's +published bulletins, then check the projected parameter values. + +Published figures (CMS SSI and spousal impoverishment bulletins, chart page): +2020 cib062420-new p.2, 2021 cib060421 p.2, 2022 cib06022022_0 p.2, +2025 cib05282025 p.2, 2026 cib04272026 p.2. """ import pytest +from policyengine_us.parameters.uprating_extensions import ( + adjust_home_equity_limit_for_inflation, + get_cms_cpi_u_percentage_increase, +) from policyengine_us.system import system +CPI = system.parameters.gov.bls.cpi HOME_EQUITY = system.parameters.gov.hhs.medicaid.eligibility.long_term_care.home_equity -CPI_U = system.parameters.gov.bls.cpi.cpi_u + +# "CPI Increase for " in each bulletin's chart. +PUBLISHED_CPI_INCREASE = {2020: 1.7, 2021: 1.4, 2022: 5.4, 2025: 2.4, 2026: 3.0} +# (minimum, maximum) home equity limits in each bulletin's chart. +PUBLISHED_LIMITS = { + 2020: (595_000, 893_000), + 2021: (603_000, 906_000), + 2022: (636_000, 955_000), + 2025: (730_000, 1_097_000), + 2026: (752_000, 1_130_000), +} + + +@pytest.mark.parametrize("year, percentage", PUBLISHED_CPI_INCREASE.items()) +def test_cpi_increase_matches_cms(year, percentage): + assert get_cms_cpi_u_percentage_increase(CPI, year) == percentage -def indexed(value, year): - change = CPI_U(f"{year - 1}-09-01") / CPI_U(f"{year - 2}-09-01") - return round(value * change / 1_000) * 1_000 +@pytest.mark.parametrize("start, end", [(2020, 2021), (2021, 2022), (2022, 2025)]) +@pytest.mark.parametrize("bound", [0, 1]) +def test_year_to_year_increases_reproduce_cms_limits(start, end, bound): + # 2021 separates the methods: $893,000 x 1.014 rounds to $906,000, while + # the unrounded 1.371% increase would give $905,000. 2022 -> 2025 chains + # through 2023 and 2024, whose bulletins are not cited here. + limit = PUBLISHED_LIMITS[start][bound] + for year in range(start + 1, end + 1): + limit = adjust_home_equity_limit_for_inflation( + limit, get_cms_cpi_u_percentage_increase(CPI, year) + ) + assert limit == PUBLISHED_LIMITS[end][bound] -@pytest.mark.parametrize("name", ["limit", "agricultural_limit"]) -def test_cms_method_reproduces_the_published_2026_limit(name): - parameter = getattr(HOME_EQUITY, name) - # CMS published $1,097,000 for 2025 and $1,130,000 for 2026. - assert indexed(parameter("2025-01-01"), 2026) == parameter("2026-01-01") +def test_a_falling_index_is_not_an_increase(): + # The not seasonally adjusted CPI-U fell from September 2014 to + # September 2015. + assert CPI.cpi_u("2015-09-01") < CPI.cpi_u("2014-09-01") + assert get_cms_cpi_u_percentage_increase(CPI, 2016) == 0 -def test_agricultural_limit_steps_only_on_january_1(): +def test_published_limits_are_parameter_values(): + for year in (2025, 2026): + assert HOME_EQUITY.limit(f"{year}-01-01") == PUBLISHED_LIMITS[year][1] + assert ( + HOME_EQUITY.agricultural_limit(f"{year}-01-01") + == (PUBLISHED_LIMITS[year][1]) + ) + + +def test_agricultural_limit_is_projected_year_to_year_on_january_1(): for year in range(2027, 2041): - prior = HOME_EQUITY.agricultural_limit(f"{year - 1}-01-01") current = HOME_EQUITY.agricultural_limit(f"{year}-01-01") - assert current == indexed(prior, year), year + assert current == adjust_home_equity_limit_for_inflation( + HOME_EQUITY.agricultural_limit(f"{year - 1}-01-01"), + get_cms_cpi_u_percentage_increase(CPI, year), + ) for month in ["02", "07", "12"]: assert HOME_EQUITY.agricultural_limit(f"{year}-{month}-01") == current -def test_2027_non_agricultural_projection_matches_the_uprated_limit(): - projected = HOME_EQUITY.limit("2027-01-01") - uprated = HOME_EQUITY.agricultural_limit("2027-01-01") - assert projected == uprated, ( - "home_equity/limit.yaml's 2027 value is a CPI-U projection; set it to " - f"{uprated:,.0f}, the uprated agricultural limit, or to the CMS figure " - "once published (and add that figure to agricultural_limit.yaml too)." +def test_non_agricultural_limit_is_the_indexed_maximum_until_the_2028_cap(): + assert HOME_EQUITY.limit("2027-01-01") == HOME_EQUITY.agricultural_limit( + "2027-01-01" ) - - -def test_non_agricultural_cap_holds_from_2028(): for year in range(2028, 2041): assert HOME_EQUITY.limit(f"{year}-01-01") == 1_000_000 assert HOME_EQUITY.agricultural_limit(f"{year}-01-01") > 1_000_000 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml index de1ebdc56e0..b6d51d2aee3 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml @@ -278,7 +278,7 @@ output: is_medicaid_long_term_care_home_equity_eligible: false -- name: Case 17, agricultural home equity limit continues to uprate after 2028. +- name: Case 17, agricultural home above the non-agricultural cap remains eligible after 2028. period: 2029 input: receives_medicaid_long_term_care_services: true @@ -355,3 +355,29 @@ output: medicaid_home_equity_limit_family_exception: [false, false] is_medicaid_long_term_care_home_equity_eligible: [false, true] + +- name: Case 21, agricultural and non-agricultural homes bracket their own limits after 2028. + period: 2029 + input: + gov.hhs.medicaid.eligibility.long_term_care.home_equity.agricultural_limit: 1_200_000 + people: + agricultural_at_limit: + receives_medicaid_long_term_care_services: true + home_is_on_agricultural_land: true + home_equity: 1_200_000 + agricultural_above_limit: + receives_medicaid_long_term_care_services: true + home_is_on_agricultural_land: true + home_equity: 1_200_001 + other_at_cap: + receives_medicaid_long_term_care_services: true + home_equity: 1_000_000 + other_above_cap: + receives_medicaid_long_term_care_services: true + home_equity: 1_000_001 + households: + household: + members: + [agricultural_at_limit, agricultural_above_limit, other_at_cap, other_above_cap] + output: + is_medicaid_long_term_care_home_equity_eligible: [true, false, true, false] From 69383adb3d956ed9ef5fe36e7b5bee28ae2707ef Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 15:32:29 -0400 Subject: [PATCH 3/4] Pin the 2029 agricultural lookup and the zero floor; fill pre-cap years by date Review r2 found that Case 21's inline override sets every year, so a lookup frozen at 2028 would still pass. A simulation test now reads the 2028 and 2029 projected limits from the parameters and checks the chassis screen at 2028 + $1, the 2029 limit and 2029 + $1; freezing the lookup at 2028 fails it. The zero-floor test now uses a synthetic 1% fall, since the 2014-15 dip already rounded to -0.0%. The non-agricultural fill now covers every unencoded year before the section 71108(c) start year rather than the gap between the two newest values, so adding a later explicit value cannot skip 2027. Co-Authored-By: Claude Opus 5.5 --- .../parameters/uprating_extensions.py | 31 +++++++---- ...est_medicaid_home_equity_limit_indexing.py | 52 +++++++++++++++++-- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 57c51e536e4..2c0c18f700a 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -615,6 +615,11 @@ def adjust_home_equity_limit_for_inflation( return float(thousands * 1_000) +# P.L. 119-21 section 71108(c): the $1,000,000 cap on non-agricultural homes +# applies beginning January 1, 2028. +HOME_EQUITY_NON_AGRICULTURAL_CAP_START_YEAR = 2028 + + def extend_medicaid_home_equity_limits( parameters: ParameterNode, end_year: int ) -> None: @@ -625,10 +630,10 @@ def extend_medicaid_home_equity_limits( CPI-U percentage (see ``get_cms_cpi_u_percentage_increase``), rounded to the nearest $1,000, effective January 1. Before P.L. 119-21 section 71108 caps other homes at $1,000,000 beginning in 2028, the agricultural and - non-agricultural limits are the same indexed maximum. So the - non-agricultural limit takes the agricultural projection for each year - between its last published value and its final value, the statutory - cap. Amounts encoded in the YAML take precedence for their own years. + non-agricultural limits are the same indexed maximum, so each year before + the cap with no non-agricultural value in the YAML takes the agricultural + projection. Amounts encoded in the YAML take precedence for their own + years. Like the other projections in this module, the values are fixed when the parameter tree is built. @@ -652,13 +657,17 @@ def extend_medicaid_home_equity_limits( value=agricultural(f"{end_year}-01-01"), ) non_agricultural = home_equity.limit - years = sorted(int(value.instant_str[:4]) for value in non_agricultural.values_list) - cap_year, last_published_year = years[-1], years[-2] - for year in range(last_published_year + 1, cap_year): - non_agricultural.update( - period=f"year:{year}-01-01:1", - value=agricultural(f"{year}-01-01"), - ) + encoded_years = { + int(value.instant_str[:4]) for value in non_agricultural.values_list + } + for year in range( + first_projected_year, HOME_EQUITY_NON_AGRICULTURAL_CAP_START_YEAR + ): + if year not in encoded_years: + non_agricultural.update( + period=f"year:{year}-01-01:1", + value=agricultural(f"{year}-01-01"), + ) def extend_wa_millionaires_standard_deduction( diff --git a/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py index ffb24cf2ece..a7d6656a1c0 100644 --- a/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py +++ b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py @@ -10,8 +10,11 @@ 2025 cib05282025 p.2, 2026 cib04272026 p.2. """ +from types import SimpleNamespace + import pytest +from policyengine_us import Simulation from policyengine_us.parameters.uprating_extensions import ( adjust_home_equity_limit_for_inflation, get_cms_cpi_u_percentage_increase, @@ -52,11 +55,24 @@ def test_year_to_year_increases_reproduce_cms_limits(start, end, bound): assert limit == PUBLISHED_LIMITS[end][bound] -def test_a_falling_index_is_not_an_increase(): - # The not seasonally adjusted CPI-U fell from September 2014 to - # September 2015. - assert CPI.cpi_u("2015-09-01") < CPI.cpi_u("2014-09-01") - assert get_cms_cpi_u_percentage_increase(CPI, 2016) == 0 +class _Series: + """A minimal stand-in for a dated CPI parameter.""" + + def __init__(self, values): + self.values = values + self.values_list = [SimpleNamespace(instant_str=date) for date in values] + + def __call__(self, date): + return self.values[date] + + +@pytest.mark.parametrize("current, percentage", [(99.0, 0), (101.0, 1.0)]) +def test_a_falling_index_is_not_an_increase(current, percentage): + cpi = SimpleNamespace( + cpi_u_nsa_september=_Series({"2030-09-01": 100.0, "2031-09-01": current}), + cpi_u=None, + ) + assert get_cms_cpi_u_percentage_increase(cpi, 2032) == percentage def test_published_limits_are_parameter_values(): @@ -86,3 +102,29 @@ def test_non_agricultural_limit_is_the_indexed_maximum_until_the_2028_cap(): for year in range(2028, 2041): assert HOME_EQUITY.limit(f"{year}-01-01") == 1_000_000 assert HOME_EQUITY.agricultural_limit(f"{year}-01-01") > 1_000_000 + + +def test_chassis_screen_reads_each_years_agricultural_limit(): + # The projected limits move with the CPI-U forecast, so the boundaries are + # read from the parameters rather than written into a YAML case. + limit_2028 = HOME_EQUITY.agricultural_limit("2028-01-01") + limit_2029 = HOME_EQUITY.agricultural_limit("2029-01-01") + assert limit_2029 > limit_2028 + equity = {"above_2028_limit": limit_2028 + 1, "at_limit": limit_2029} + equity["above_limit"] = limit_2029 + 1 + people = { + name: { + "receives_medicaid_long_term_care_services": {"2029": True}, + "home_is_on_agricultural_land": {"2029": True}, + "home_equity": {"2029": value}, + } + for name, value in equity.items() + } + situation = {"people": people} + for group in ["tax_units", "spm_units", "families", "marital_units", "households"]: + situation[group] = {f"{name}_{group}": {"members": [name]} for name in people} + simulation = Simulation(situation=situation) + eligible = simulation.calculate( + "is_medicaid_long_term_care_home_equity_eligible", 2029 + ) + assert eligible.tolist() == [True, True, False] From 843ba057b8751ac276a658a77636b7ac34601768 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 15:57:19 -0400 Subject: [PATCH 4/4] Fill unencoded pre-cap home equity years from the limit's own coverage Review r3: starting the non-agricultural fill at the agricultural limit's first projected year would skip 2027 if CMS's 2027 agricultural value were encoded before the non-agricultural one, leaving 2027 at the 2026 value. The fill now starts after the non-agricultural limit's earliest encoded year. A regression on a small parameter tree covers both orders; the old range fails it. Co-Authored-By: Claude Opus 5.5 --- .../parameters/uprating_extensions.py | 2 +- ...est_medicaid_home_equity_limit_indexing.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 2c0c18f700a..5e435c01d14 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -661,7 +661,7 @@ def extend_medicaid_home_equity_limits( int(value.instant_str[:4]) for value in non_agricultural.values_list } for year in range( - first_projected_year, HOME_EQUITY_NON_AGRICULTURAL_CAP_START_YEAR + min(encoded_years) + 1, HOME_EQUITY_NON_AGRICULTURAL_CAP_START_YEAR ): if year not in encoded_years: non_agricultural.update( diff --git a/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py index a7d6656a1c0..81fb207e5d2 100644 --- a/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py +++ b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py @@ -15,8 +15,11 @@ import pytest from policyengine_us import Simulation +from policyengine_core.parameters import ParameterNode + from policyengine_us.parameters.uprating_extensions import ( adjust_home_equity_limit_for_inflation, + extend_medicaid_home_equity_limits, get_cms_cpi_u_percentage_increase, ) from policyengine_us.system import system @@ -128,3 +131,58 @@ def test_chassis_screen_reads_each_years_agricultural_limit(): "is_medicaid_long_term_care_home_equity_eligible", 2029 ) assert eligible.tolist() == [True, True, False] + + +@pytest.mark.parametrize("agricultural_published_through", [2026, 2027]) +def test_non_agricultural_fill_follows_its_own_coverage( + agricultural_published_through, +): + # Publishing the 2027 agricultural limit before the non-agricultural one + # must not leave the non-agricultural 2027 limit at its 2026 value. + agricultural = {"2025-01-01": 1_097_000, "2026-01-01": 1_130_000} + if agricultural_published_through == 2027: + agricultural["2027-01-01"] = 1_164_000 + non_agricultural = { + "2025-01-01": 1_097_000, + "2026-01-01": 1_130_000, + "2028-01-01": 1_000_000, + } + septembers = { + f"{year}-09-01": 100 * 1.03 ** (year - 2024) for year in range(2024, 2031) + } + parameters = ParameterNode( + data={ + "gov": { + "hhs": { + "medicaid": { + "eligibility": { + "long_term_care": { + "home_equity": { + "agricultural_limit": {"values": agricultural}, + "limit": {"values": non_agricultural}, + } + } + } + } + }, + "bls": { + "cpi": { + "cpi_u_nsa_september": {"values": septembers}, + "cpi_u": {"values": septembers}, + } + }, + } + } + ) + extend_medicaid_home_equity_limits(parameters, end_year=2030) + home_equity = parameters.gov.hhs.medicaid.eligibility.long_term_care.home_equity + assert home_equity.limit("2027-01-01") == home_equity.agricultural_limit( + "2027-01-01" + ) + assert home_equity.limit("2027-01-01") > 1_130_000 + assert home_equity.limit("2028-01-01") == 1_000_000 + assert home_equity.agricultural_limit( + "2028-01-01" + ) == adjust_home_equity_limit_for_inflation( + home_equity.agricultural_limit("2027-01-01"), 3.0 + )