From f3a5d1a174a5574f57e8900bb47b1151c1150834 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 25 Jul 2026 09:34:48 -0400 Subject: [PATCH 1/4] Fix the capital gains realisation response The response measured the baseline marginal rate on a branch of the reform simulation. Simulations hold their baseline as a separately constructed simulation rather than a branch, so both sides of the comparison carried reform parameters and the measured rate change was zero for every reform, leaving the elasticity with no effect on any costing. Issue #1319 fixed the input plumbing for this; the measurement itself still reported no change. Measure the baseline rate in the baseline simulation, and clone the tax-benefit system for the measurement branches so neutralising the response variable cannot reach the simulation being measured. Define the elasticity against the retention rate rather than the tax rate, which is how the empirical estimates are reported, so a published value can be entered as published. Under the previous convention a positive elasticity raised realisations when rates rose. Co-Authored-By: Claude Opus 5 --- .../capital-gains-realisation-response.md | 2 + .../capital_gains_responses/elasticity.yaml | 7 +- .../tests/test_capital_gains_responses.py | 109 ++++++++++++++++++ policyengine_uk/utils/capital_gains.py | 50 ++++++++ .../capital_gains_behavioural_response.py | 11 +- .../relative_capital_gains_mtr_change.py | 35 +----- ...ive_capital_gains_retention_rate_change.py | 27 +++++ 7 files changed, 205 insertions(+), 36 deletions(-) create mode 100644 changelog.d/capital-gains-realisation-response.md create mode 100644 policyengine_uk/tests/test_capital_gains_responses.py create mode 100644 policyengine_uk/utils/capital_gains.py create mode 100644 policyengine_uk/variables/gov/hmrc/capital_gains_tax/relative_capital_gains_retention_rate_change.py diff --git a/changelog.d/capital-gains-realisation-response.md b/changelog.d/capital-gains-realisation-response.md new file mode 100644 index 000000000..fa15054ff --- /dev/null +++ b/changelog.d/capital-gains-realisation-response.md @@ -0,0 +1,2 @@ +- Fixed the capital gains realisation response, which measured the baseline marginal rate on a branch of the reform simulation and so reported no rate change for any reform, leaving the elasticity with no effect on any costing. +- Defined the capital gains elasticity against the retention rate rather than the tax rate, matching how published estimates are reported, and added `relative_capital_gains_retention_rate_change`. diff --git a/policyengine_uk/parameters/gov/simulation/capital_gains_responses/elasticity.yaml b/policyengine_uk/parameters/gov/simulation/capital_gains_responses/elasticity.yaml index c351575c1..7a1c20a26 100644 --- a/policyengine_uk/parameters/gov/simulation/capital_gains_responses/elasticity.yaml +++ b/policyengine_uk/parameters/gov/simulation/capital_gains_responses/elasticity.yaml @@ -1,6 +1,11 @@ -description: Elasticity of capital gains with respect to the capital gains marginal tax rate. +description: Elasticity of capital gains realisations with respect to the capital gains retention rate, the share of a marginal pound of gains kept after tax. Higher values mean a given rate rise reduces realisations by more. Published estimates span roughly 0.5 to 2. values: 2000-01-01: 0 metadata: unit: /1 label: Capital gains elasticity + reference: + - title: Agersnap and Zidar (2021), The tax elasticity of capital gains and revenue-maximizing rates + href: https://www.aeaweb.org/articles?id=10.1257/aeri.20200535 + - title: Advani, Lonsdale and Summers (2024), Reforming Capital Gains Tax + href: https://centax.org.uk/wp-content/uploads/2024/10/AdvaniLonsdaleSummers2024_CGTReform.pdf diff --git a/policyengine_uk/tests/test_capital_gains_responses.py b/policyengine_uk/tests/test_capital_gains_responses.py new file mode 100644 index 000000000..6451f2423 --- /dev/null +++ b/policyengine_uk/tests/test_capital_gains_responses.py @@ -0,0 +1,109 @@ +"""Tests for the capital gains realisation response to CGT rate changes.""" + +import pytest + +from policyengine_uk import Microsimulation +from policyengine_uk.model_api import Scenario + +YEAR = 2026 + +SITUATION = { + "people": { + "person": { + "age": {YEAR: 45}, + "employment_income": {YEAR: 100_000}, + "capital_gains": {YEAR: 200_000}, + } + }, + "benunits": {"benunit": {"members": ["person"]}}, + "households": {"household": {"members": ["person"]}}, +} + +EQUALISED_RATES = { + "gov.hmrc.cgt.basic_rate": {str(YEAR): 0.20}, + "gov.hmrc.cgt.higher_rate": {str(YEAR): 0.40}, + "gov.hmrc.cgt.additional_rate": {str(YEAR): 0.45}, +} + + +def simulate(elasticity: float | None = None, rates: bool = True) -> Microsimulation: + changes = dict(EQUALISED_RATES) if rates else {} + if elasticity is not None: + changes["gov.simulation.capital_gains_responses.elasticity"] = { + str(YEAR): elasticity + } + if not changes: + return Microsimulation(situation=SITUATION) + return Microsimulation( + situation=SITUATION, scenario=Scenario(parameter_changes=changes) + ) + + +def test_rate_rise_registers_against_the_baseline(): + """A CGT rate rise registers as a higher rate and a lower retention rate. + + Regression test for measuring the baseline against a branch of the reform + simulation, which reported no rate change for any reform (issue #1319). + """ + sim = simulate(elasticity=1.0) + mtr_change = sim.calculate("relative_capital_gains_mtr_change", YEAR).values[0] + retention_change = sim.calculate( + "relative_capital_gains_retention_rate_change", YEAR + ).values[0] + + assert mtr_change > 0, f"expected a positive log rate change, got {mtr_change}" + assert retention_change < 0, ( + f"expected a negative log retention change, got {retention_change}" + ) + + +def test_realisations_fall_when_rates_rise(): + """Gains fall under a rate rise, by more at a larger elasticity.""" + baseline_gains = simulate(rates=False).calculate("capital_gains", YEAR).sum() + + modest = simulate(elasticity=0.5).calculate("capital_gains", YEAR).sum() + large = simulate(elasticity=1.0).calculate("capital_gains", YEAR).sum() + + assert modest < baseline_gains + assert large < modest + + +def test_revenue_falls_short_of_the_static_estimate(): + """The behavioural response costs revenue relative to a static costing.""" + static = simulate(elasticity=0).calculate("capital_gains_tax", YEAR).sum() + dynamic = simulate(elasticity=1.0).calculate("capital_gains_tax", YEAR).sum() + + assert dynamic < static + assert dynamic > 0 + + +def test_zero_elasticity_leaves_gains_unchanged(): + """The default elasticity of zero keeps costings static.""" + sim = simulate(elasticity=0) + response = sim.calculate("capital_gains_behavioural_response", YEAR).sum() + + assert response == 0 + + +def test_no_reform_produces_no_response(): + """A simulation with no reform reports no realisation response.""" + sim = Microsimulation( + situation=SITUATION, + scenario=Scenario( + parameter_changes={ + "gov.simulation.capital_gains_responses.elasticity": {str(YEAR): 1.0} + } + ), + ) + response = sim.calculate("capital_gains_behavioural_response", YEAR).sum() + + assert response == 0 + + +def test_measurement_leaves_the_response_variable_active(): + """Measuring the rate change does not neutralise the response itself.""" + sim = simulate(elasticity=1.0) + sim.calculate("relative_capital_gains_mtr_change", YEAR) + variable = sim.tax_benefit_system.variables["capital_gains_behavioural_response"] + + assert variable.formula is not None diff --git a/policyengine_uk/utils/capital_gains.py b/policyengine_uk/utils/capital_gains.py new file mode 100644 index 000000000..f65bfae74 --- /dev/null +++ b/policyengine_uk/utils/capital_gains.py @@ -0,0 +1,50 @@ +"""Measurement of capital gains marginal tax rates against the baseline.""" + +import numpy as np + +from policyengine_core.simulations import Simulation + + +def measure_mtr( + simulation: Simulation, + branch_name: str, + period, + gains: np.ndarray, +) -> np.ndarray: + """Measure the capital gains MTR in a simulation, holding gains fixed. + + The branch clones the tax-benefit system because it neutralises the + behavioural response variable, which would otherwise recurse back into + this measurement. Cloning keeps that neutralisation off the simulation + being measured. + """ + branch = simulation.get_branch(branch_name, clone_system=True) + branch.tax_benefit_system.neutralize_variable("capital_gains_behavioural_response") + branch.set_input("capital_gains_before_response", period, gains) + mtr = branch.populations["person"]("marginal_tax_rate_on_capital_gains", period) + del simulation.branches[branch_name] + return mtr + + +def measure_capital_gains_mtrs(person, period) -> tuple[np.ndarray, np.ndarray]: + """Return the reform and baseline capital gains MTRs for each person. + + Both rates are measured at the same level of gains, so the difference + reflects the reform alone. Returns two zero arrays where the simulation + has no baseline to compare against. + + Simulations hold their baseline as a separately constructed simulation + rather than a branch, so the baseline rate has to be measured there. A + branch of the reform simulation carries reform parameters, and reports no + rate change however large the reform. + """ + simulation: Simulation = person.simulation + baseline = simulation.baseline + if baseline is None: + zeros = np.zeros(person.count) + return zeros, zeros + + gains = person("capital_gains_before_response", period) + reform_mtr = measure_mtr(simulation, "cgr_measurement", period, gains) + baseline_mtr = measure_mtr(baseline, "baseline_cgr_measurement", period, gains) + return reform_mtr, baseline_mtr diff --git a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_behavioural_response.py b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_behavioural_response.py index 40829f155..8bbff34df 100644 --- a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_behavioural_response.py +++ b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_behavioural_response.py @@ -6,6 +6,11 @@ class capital_gains_behavioural_response(Variable): value_type = float entity = Person label = "capital gains behavioral response" + documentation = ( + "Change in realised gains under a reform to the taxation of gains, " + "given the assumed elasticity of realisations with respect to the " + "retention rate." + ) unit = GBP definition_period = YEAR @@ -18,11 +23,13 @@ def formula(person, period, parameters): return 0 capital_gains = person("capital_gains_before_response", period) - tax_rate_change = person("relative_capital_gains_mtr_change", period) + retention_rate_change = person( + "relative_capital_gains_retention_rate_change", period + ) elasticity = person("capital_gains_elasticity", period) # Calculate response using log differences - response_factor = np.exp(elasticity * tax_rate_change) - 1 + response_factor = np.exp(elasticity * retention_rate_change) - 1 response = capital_gains * response_factor return response diff --git a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/relative_capital_gains_mtr_change.py b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/relative_capital_gains_mtr_change.py index 4cef0119d..5e097c434 100644 --- a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/relative_capital_gains_mtr_change.py +++ b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/relative_capital_gains_mtr_change.py @@ -1,5 +1,6 @@ from policyengine_uk.model_api import * from policyengine_core.simulations import * +from policyengine_uk.utils.capital_gains import measure_capital_gains_mtrs class relative_capital_gains_mtr_change(Variable): @@ -10,39 +11,7 @@ class relative_capital_gains_mtr_change(Variable): definition_period = YEAR def formula(person, period, parameters): - simulation: Simulation = person.simulation - baseline_branch = simulation.get_branch("baseline").get_branch( - "baseline_cgr_measurement" - ) - baseline_branch.set_input( - "capital_gains_before_response", - period, - person("capital_gains_before_response", period), - ) - baseline_person = baseline_branch.populations["person"] - baseline_branch.tax_benefit_system.neutralize_variable( - "capital_gains_behavioural_response" - ) - baseline_branch.set_input( - "capital_gains_before_response", - period, - person("capital_gains_before_response", period), - ) - baseline_mtr = baseline_person("marginal_tax_rate_on_capital_gains", period) - del simulation.branches["baseline"].branches["baseline_cgr_measurement"] - - measurement_branch = simulation.get_branch("cgr_measurement") - measurement_branch.tax_benefit_system.neutralize_variable( - "capital_gains_behavioural_response" - ) - measurement_branch.set_input( - "capital_gains_before_response", - period, - person("capital_gains_before_response", period), - ) - measurement_person = measurement_branch.populations["person"] - reform_mtr = measurement_person("marginal_tax_rate_on_capital_gains", period) - del simulation.branches["cgr_measurement"] + reform_mtr, baseline_mtr = measure_capital_gains_mtrs(person, period) # Handle zeros in tax rates to prevent log(0) min_rate = 0.001 diff --git a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/relative_capital_gains_retention_rate_change.py b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/relative_capital_gains_retention_rate_change.py new file mode 100644 index 000000000..ee35879bb --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/relative_capital_gains_retention_rate_change.py @@ -0,0 +1,27 @@ +from policyengine_uk.model_api import * +from policyengine_core.simulations import * +from policyengine_uk.utils.capital_gains import measure_capital_gains_mtrs + + +class relative_capital_gains_retention_rate_change(Variable): + value_type = float + entity = Person + label = "relative change in the capital gains retention rate" + documentation = ( + "Log change in the share of a marginal pound of gains kept after tax. " + "The empirical literature estimates realisation elasticities against " + "this retention rate rather than against the tax rate." + ) + unit = "/1" + definition_period = YEAR + + def formula(person, period, parameters): + reform_mtr, baseline_mtr = measure_capital_gains_mtrs(person, period) + + # Floor the retention rate to keep the log defined where a marginal + # pound of gains is taxed away in full. + min_retention_rate = 0.001 + baseline_retention = np.maximum(1 - baseline_mtr, min_retention_rate) + reform_retention = np.maximum(1 - reform_mtr, min_retention_rate) + + return np.log(reform_retention) - np.log(baseline_retention) From 47e82e4cbe2eba0874cebd6746993addd7f9c4ab Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 12 Aug 2026 17:11:34 -0400 Subject: [PATCH 2/4] Refuse to neutralise on a shared system; prove rank-2 symmetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review found that a branch pre-created under the measurement's name shares the simulation's own tax-benefit system, and get_branch returns it without honouring clone_system — so the measurement's neutralise swapped the live response variable for a neutralised wrapper on the parent, zeroing every later recalculation. The measurement now sidesteps any existing name and refuses outright if the branch system is not a clone. The variable-stays-active test asserts object identity rather than formula presence, which a neutralised wrapper also carries. The review also reported a second household gainer losing its response. That does not reproduce independently — two equal gainers respond symmetrically — and is consistent with observing recalculations after the neutralisation leak above. A regression test pins the symmetry. Co-Authored-By: Claude Fable 5 --- .../capital-gains-realisation-response.md | 1 + .../tests/test_capital_gains_responses.py | 61 ++++++++++++++++++- policyengine_uk/utils/capital_gains.py | 11 ++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/changelog.d/capital-gains-realisation-response.md b/changelog.d/capital-gains-realisation-response.md index fa15054ff..cc11adb15 100644 --- a/changelog.d/capital-gains-realisation-response.md +++ b/changelog.d/capital-gains-realisation-response.md @@ -1,2 +1,3 @@ - Fixed the capital gains realisation response, which measured the baseline marginal rate on a branch of the reform simulation and so reported no rate change for any reform, leaving the elasticity with no effect on any costing. - Defined the capital gains elasticity against the retention rate rather than the tax rate, matching how published estimates are reported, and added `relative_capital_gains_retention_rate_change`. +- Hardened the measurement against a pre-created branch of the same name, which shared the simulation's tax-benefit system and let the neutralised response variable replace the live one for every later recalculation. diff --git a/policyengine_uk/tests/test_capital_gains_responses.py b/policyengine_uk/tests/test_capital_gains_responses.py index 6451f2423..f1486814f 100644 --- a/policyengine_uk/tests/test_capital_gains_responses.py +++ b/policyengine_uk/tests/test_capital_gains_responses.py @@ -101,9 +101,64 @@ def test_no_reform_produces_no_response(): def test_measurement_leaves_the_response_variable_active(): - """Measuring the rate change does not neutralise the response itself.""" + """Measuring the rate change does not neutralise the response itself. + + Object identity, not formula presence: a neutralised wrapper still + carries a formula, so the old assertion could not see the damage. + """ sim = simulate(elasticity=1.0) + before = sim.tax_benefit_system.variables["capital_gains_behavioural_response"] sim.calculate("relative_capital_gains_mtr_change", YEAR) - variable = sim.tax_benefit_system.variables["capital_gains_behavioural_response"] + after = sim.tax_benefit_system.variables["capital_gains_behavioural_response"] + + assert after is before + + +def test_pre_created_measurement_branch_cannot_poison_the_system(): + """A branch pre-created under the measurement's name shares the parent + system, and get_branch returns it without honouring clone_system — so + neutralising there would disable the response for every later + recalculation. The measurement must sidestep the name instead.""" + sim = simulate(elasticity=1.0) + sim.get_branch("cgr_measurement") + before = sim.tax_benefit_system.variables["capital_gains_behavioural_response"] + + response = sim.calculate("capital_gains_behavioural_response", YEAR).sum() + after = sim.tax_benefit_system.variables["capital_gains_behavioural_response"] + + assert response < 0 + assert after is before + + +def test_two_gainers_in_one_household_respond_symmetrically(): + """Equal gainers get equal responses; the second adult is not dropped.""" + situation = { + "people": { + "first": { + "age": {YEAR: 45}, + "employment_income": {YEAR: 100_000}, + "capital_gains": {YEAR: 200_000}, + }, + "second": { + "age": {YEAR: 44}, + "employment_income": {YEAR: 100_000}, + "capital_gains": {YEAR: 200_000}, + }, + }, + "benunits": {"benunit": {"members": ["first", "second"]}}, + "households": {"household": {"members": ["first", "second"]}}, + } + sim = Microsimulation( + situation=situation, + scenario=Scenario( + parameter_changes={ + **EQUALISED_RATES, + "gov.simulation.capital_gains_responses.elasticity": {str(YEAR): 1.0}, + } + ), + ) + + responses = sim.calculate("capital_gains_behavioural_response", YEAR).values - assert variable.formula is not None + assert responses[0] < 0 + assert responses[0] == pytest.approx(responses[1]) diff --git a/policyengine_uk/utils/capital_gains.py b/policyengine_uk/utils/capital_gains.py index f65bfae74..155a701f5 100644 --- a/policyengine_uk/utils/capital_gains.py +++ b/policyengine_uk/utils/capital_gains.py @@ -18,7 +18,18 @@ def measure_mtr( this measurement. Cloning keeps that neutralisation off the simulation being measured. """ + # get_branch returns an existing branch of the requested name without + # honouring clone_system, and neutralising on a shared system would + # permanently disable the response variable for the caller. Take a name + # nothing else holds, then require the clone before touching it. + while branch_name in simulation.branches: + branch_name += "_" branch = simulation.get_branch(branch_name, clone_system=True) + if branch.tax_benefit_system is simulation.tax_benefit_system: + raise RuntimeError( + "Capital gains MTR measurement requires a cloned tax-benefit " + "system; refusing to neutralise on the simulation's own." + ) branch.tax_benefit_system.neutralize_variable("capital_gains_behavioural_response") branch.set_input("capital_gains_before_response", period, gains) mtr = branch.populations["person"]("marginal_tax_rate_on_capital_gains", period) From 4b754e5a65e54ed216b242eb5c8798ffe2671277 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 13 Aug 2026 00:59:21 -0400 Subject: [PATCH 3/4] Require policyengine-core 3.30.1 for nested-branch cache deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second household gainer's response computed as zero under the locked core 3.26.0 while newer environments passed: bisection lands the change at core 3.30.1, whose changelog entry is "Fixed nested simulation cache deletion so perturbation branches recompute inherited values without mutating parent storage" — exactly the per-adult perturbation reuse the review reported. Raise the floor to 3.30.1 and relock (resolves 3.30.4), so the committed environment and any fresh resolution both carry the fix, and the symmetry test guards it. Co-Authored-By: Claude Fable 5 --- changelog.d/capital-gains-realisation-response.md | 1 + pyproject.toml | 2 +- uv.lock | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/changelog.d/capital-gains-realisation-response.md b/changelog.d/capital-gains-realisation-response.md index cc11adb15..4e05998c3 100644 --- a/changelog.d/capital-gains-realisation-response.md +++ b/changelog.d/capital-gains-realisation-response.md @@ -1,3 +1,4 @@ - Fixed the capital gains realisation response, which measured the baseline marginal rate on a branch of the reform simulation and so reported no rate change for any reform, leaving the elasticity with no effect on any costing. - Defined the capital gains elasticity against the retention rate rather than the tax rate, matching how published estimates are reported, and added `relative_capital_gains_retention_rate_change`. - Hardened the measurement against a pre-created branch of the same name, which shared the simulation's tax-benefit system and let the neutralised response variable replace the live one for every later recalculation. +- Raised the policyengine-core floor to 3.30.1, whose nested-simulation cache fix lets a second household gainer's response compute; a symmetry regression test pins it. diff --git a/pyproject.toml b/pyproject.toml index 377dc1adc..af9027f45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "policyengine-core>=3.26.0", + "policyengine-core>=3.30.1", "microdf-python>=1.2.1", "pydantic>=2.11.7", "tables>=3.9.2,<3.10.2; python_version < '3.10'", diff --git a/uv.lock b/uv.lock index a1db86a16..ca6d97611 100644 --- a/uv.lock +++ b/uv.lock @@ -1557,7 +1557,7 @@ wheels = [ [[package]] name = "policyengine-core" -version = "3.26.0" +version = "3.30.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath", marker = "python_full_version >= '3.11'" }, @@ -1577,14 +1577,14 @@ dependencies = [ { name = "standard-imghdr", marker = "python_full_version >= '3.11'" }, { name = "wheel", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/69/adb6407c97de5260a938344f9eafa9979bf8f97aec8c628538d906ecdec2/policyengine_core-3.26.0.tar.gz", hash = "sha256:a571026ef418653ec18f087463cf37e9be730e90ad4376cb10997f0ddf9f8eda", size = 468190, upload-time = "2026-05-04T19:26:27.707Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/ce/850539b176dfbbb7e8ca4ece80c00cd880600d2fe55cc1718c22600d4df2/policyengine_core-3.30.4.tar.gz", hash = "sha256:6c1573d9486b291f5104bb275bb26c2668f9e604771b36b83e641363c14d3a1a", size = 502330, upload-time = "2026-08-04T13:44:46.299Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/f3/0e98b30d4eb7b309c3f1f1d8c2354595f78319ce2442eda069f02a47f4d1/policyengine_core-3.26.0-py3-none-any.whl", hash = "sha256:d63a4622233b61c4c5fc64d4f65030d65b2564ac63ac87b17d545d63cdf17194", size = 232135, upload-time = "2026-05-04T19:26:25.693Z" }, + { url = "https://files.pythonhosted.org/packages/61/bd/62801802dfbf7244e9d8e53432d4581ce71d2e308742f9109a7c3d9d21a1/policyengine_core-3.30.4-py3-none-any.whl", hash = "sha256:a3edd4c528a048f8d075fee31231866d536d523cbcdeca78ad45d75bc3d5758a", size = 245473, upload-time = "2026-08-04T13:44:44.987Z" }, ] [[package]] name = "policyengine-uk" -version = "2.89.4" +version = "2.90.0" source = { editable = "." } dependencies = [ { name = "microdf-python", marker = "python_full_version >= '3.11'" }, @@ -1615,7 +1615,7 @@ requires-dist = [ { name = "furo", marker = "extra == 'dev'", specifier = "<2023" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=2.0.0a0" }, { name = "microdf-python", specifier = ">=1.2.1" }, - { name = "policyengine-core", specifier = ">=3.26.0" }, + { name = "policyengine-core", specifier = ">=3.30.1" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "rich", marker = "extra == 'dev'" }, From f06f73882adf58243b0091ce2c8f49f73ce28d70 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 13 Aug 2026 01:15:57 -0400 Subject: [PATCH 4/4] Relock after the branch update carried main's version bump The merge from main moved the project version to 2.90.1 while the lock still recorded 2.90.0, so a locked sync refused the tip. Co-Authored-By: Claude Fable 5 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index ca6d97611..b98afea1e 100644 --- a/uv.lock +++ b/uv.lock @@ -1584,7 +1584,7 @@ wheels = [ [[package]] name = "policyengine-uk" -version = "2.90.0" +version = "2.90.1" source = { editable = "." } dependencies = [ { name = "microdf-python", marker = "python_full_version >= '3.11'" },