policyengine 6.4.2__tar.gz → 6.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/workflows/pr_code_changes.yaml +4 -3
- {policyengine-6.4.2 → policyengine-6.5.0}/CHANGELOG.md +29 -0
- {policyengine-6.4.2/src/policyengine.egg-info → policyengine-6.5.0}/PKG-INFO +3 -2
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/_quarto.yml +1 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/dev.md +1 -1
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/households.md +2 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/microsim.md +54 -1
- policyengine-6.5.0/docs/receipts.md +185 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/run-records.md +1 -1
- {policyengine-6.4.2 → policyengine-6.5.0}/pyproject.toml +6 -2
- policyengine-6.5.0/scripts/generate_receipt_schemas.py +60 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/__init__.py +8 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/__init__.py +1 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/run_record.py +24 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/simulation.py +30 -6
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/tax_benefit_model_version.py +12 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/data/bundle/manifest.json +10 -10
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/data/bundle/uk.trace.tro.jsonld +5 -5
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/data/bundle/us.trace.tro.jsonld +5 -5
- policyengine-6.5.0/src/policyengine/data/schemas/execution_receipt.v1.schema.json +720 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/__init__.py +21 -0
- policyengine-6.5.0/src/policyengine/provenance/bytecode.py +179 -0
- policyengine-6.5.0/src/policyengine/provenance/canonical.py +200 -0
- policyengine-6.5.0/src/policyengine/provenance/loaded.py +34 -0
- policyengine-6.5.0/src/policyengine/provenance/receipt.py +3674 -0
- policyengine-6.5.0/src/policyengine/provenance/wheels.py +438 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/common/model_version.py +61 -5
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/uk/budget_reform_contract.py +349 -28
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/uk/budget_reform_population.py +9 -5
- policyengine-6.5.0/src/policyengine/tax_benefit_models/uk/capital_gains_responses.py +321 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/uk/datasets.py +22 -2
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/uk/household.py +13 -4
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/uk/model.py +16 -0
- {policyengine-6.4.2 → policyengine-6.5.0/src/policyengine.egg-info}/PKG-INFO +3 -2
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine.egg-info/SOURCES.txt +22 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine.egg-info/requires.txt +2 -1
- policyengine-6.5.0/tests/fixtures/uk_contract/evaluate_case_rows.json +1 -0
- policyengine-6.5.0/tests/fixtures/uk_replay/DIGESTS.json +8 -0
- policyengine-6.5.0/tests/fixtures/uk_replay/README.md +10 -0
- policyengine-6.5.0/tests/fixtures/uk_replay/qualification_request_golden.jcs +1 -0
- policyengine-6.5.0/tests/fixtures/uk_replay/qualification_request_golden.json +72 -0
- policyengine-6.5.0/tests/fixtures/uk_replay/replay_receipt_golden.jcs +1 -0
- policyengine-6.5.0/tests/fixtures/uk_replay/replay_receipt_golden.json +137 -0
- policyengine-6.5.0/tests/receipt_modifiers.py +25 -0
- policyengine-6.5.0/tests/receipt_unbindable_modifiers.py +16 -0
- policyengine-6.5.0/tests/test_canonical_json.py +231 -0
- policyengine-6.5.0/tests/test_execution_receipt.py +6401 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_household_calculator_snapshot.py +5 -1
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_uk_budget_reform_contract.py +357 -0
- policyengine-6.5.0/tests/test_uk_cgt_response_records.py +231 -0
- policyengine-6.5.0/tests/test_uk_cgt_responses.py +432 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_uk_dated_reforms.py +12 -1
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_uk_year_file_data_year.py +10 -0
- policyengine-6.5.0/tests/test_wheels.py +853 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/uv.lock +13 -2
- policyengine-6.4.2/tests/fixtures/uk_contract/evaluate_case_rows.json +0 -1
- {policyengine-6.4.2 → policyengine-6.5.0}/.claude/policyengine-guide.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.claude/quick-reference.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/CONTRIBUTING.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/bump_version.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/capture-version.sh +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/changelog_template.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/check-changelog.sh +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/copilot-instructions.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/dispatch-policyengine-release.sh +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/fetch_version.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/get-changelog-diff.sh +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/has-functional-changes.sh +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/is-version-number-acceptable.sh +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/publish-git-tag.sh +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/wait-for-pypi.sh +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/workflows/draft-pdf.yml +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/workflows/pr_docs_changes.yaml +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.github/workflows/push.yaml +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.gitignore +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/.python-version +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/AGENTS.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/CITATION.cff +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/CLAUDE.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/CODE_OF_CONDUCT.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/CONTRIBUTING.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/LICENSE +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/Makefile +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/README.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/architecture.png +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/architecture.svg +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/changelog.d/.gitkeep +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/.gitignore +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/_generator/README.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/_generator/build_reference.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/bundles.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/countries.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/data-publishing-design.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/decile-impact-spec.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/disclosure.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/engineering/runbooks/acs-local-wic-replacement.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/engineering/runbooks/build-m-us-populace-certification.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/engineering/runbooks/canonical-spm-development.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/engineering/runbooks/us-legacy-spm-sources.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/engineering/skills/README.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/engineering/skills/data-certification.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/engineering/skills/github-prs.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/engineering/skills/repository-guidance.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/examples.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/getting-started.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/impact-analysis.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/index.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/methodology/index.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/methodology/model-architecture.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/methodology/us-health-costs.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/outputs.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/programs/index.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/programs/us-chip.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/reference/index.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/reforms.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/regions.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/release-bundles.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/trace-case-study.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/docs/visualisation.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/belgium_axiom_pilot.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/employment_income_variation_uk.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/employment_income_variation_us.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/household_impact_example.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/income_bands_uk.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/income_distribution_us.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/paper_repro_uk.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/policy_change_uk.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/speedtest_us_simulation.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/uk_geography_disclosure.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/examples/us_budgetary_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/paper-preview.html +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/paper.bib +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/paper.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/bundle.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/certify_data_release.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/check_data_staleness.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/check_native_spm_loading.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/check_release_credentials.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/export_bundle_release_assets.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/generate_bundle_artifacts.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/generate_trace_tros.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/prepare_package_bundle_update.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/release_lock.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/scripts/spm_bundle.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/setup.cfg +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/bundle.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/cli.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/cache.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/dataset.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/dynamic.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/output.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/parameter.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/parameter_node.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/parameter_value.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/policy.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/region.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/scoping_strategy.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/spm.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/tax_benefit_model.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/core/variable.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/uk/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/uk/regions.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/us/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/us/data/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/us/data/districts.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/us/data/places.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/us/data/states.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/countries/us/regions.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/data/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/data/schemas/trace_tro.schema.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/data/uk_geography_assets.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/derivations/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/derivations/narrate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/derivations/trace.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/__main__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/areas.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/estimate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/noise.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/release.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/schema.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/support.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/disclosure/validate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/graph/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/graph/extractor.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/graph/graph.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/aggregate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/change_aggregate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/cliff_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/congressional_district_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/constituency_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/decile_analysis.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/decile_grouping.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/decile_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/extra_variables.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/inequality.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/intra_decile_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/labor_supply_response.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/local_authority_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/poverty.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/program_statistics.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/uk_geography_assets.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/uk_geography_disclosure.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/outputs/uk_geography_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/bundle_validation.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/certification.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/dataset_materialization.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/manifest.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/pyproject_pins.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/spm_configuration.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/trace.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/verify.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/provenance/zenodo.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/results/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/results/schema.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/results/trace_tro.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/results/tracking.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/be/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/be/datasets.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/be/model.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/common/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/common/axes.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/common/extra_variables.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/common/household.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/common/reform.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/common/result.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/uk/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/uk/analysis.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/analysis.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/datasets.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/household.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_inputs.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_spm/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_spm/declarations.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_spm/independence.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_spm/model_adapter.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_spm/record.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_spm/registry.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_spm/runs.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/legacy_spm/universe.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/model.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/tax_benefit_models/us/spm.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/dates.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/design.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/entity_utils.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/errors.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/hashing.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/household_validation.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/parameter_labels.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/parametric_reforms.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine/utils/plotting.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine.egg-info/dependency_links.txt +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine.egg-info/entry_points.txt +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/src/policyengine.egg-info/top_level.txt +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/conftest.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/filtering_fixtures.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/uk_couple_two_kids.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/uk_model_surface.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/uk_single_adult_employment_income.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/uk_single_adult_no_income.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/uk_single_parent_one_child.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/us_married_two_kids_high_income.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/us_model_surface.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/us_single_adult_employment_income.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/us_single_adult_no_income.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/household_calculator_snapshots/us_single_parent_one_child.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/parameter_labels_fixtures.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/parametric_reforms_fixtures.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/poverty_by_demographics_fixtures.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/region_fixtures.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/release_tros/README.md +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/release_tros/country-models.lock.toml +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/release_tros/manifest.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/release_tros/us-release_manifest.json +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/release_tros/us.trace.tro.jsonld +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/spm_development.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/us_reform_fixtures.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/fixtures/variable_label_fixtures.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_aggregate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_be_axiom_pilot.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_budgetary_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_bump_version.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_bundle.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_bundle_metadata.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_cache.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_certify_data_release.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_change_aggregate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_cliff_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_cliff_impact_analysis.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_congressional_district_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_constituency_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_dataset_materialization.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_dataset_persistence.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_dataset_runtime.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_dataset_source.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_decile_grouping.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_derivations.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_dict_reforms_on_simulation.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_disclosure_areas.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_disclosure_estimate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_disclosure_noise.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_disclosure_release.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_disclosure_schema.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_disclosure_support.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_disclosure_validate.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_entity_mapping.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_entity_utils.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_entity_utils_characterization.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_errors.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_extra_variables.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_filtering.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_generate_trace_tros.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_graph/__init__.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_graph/conftest.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_graph/test_extractor.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_household_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_inequality.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_intra_decile_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_labor_supply_response.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_local_authority_impact.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_manifest_version_mismatch.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_models.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_pandas3_compatibility.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_parameter_labels.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_parameter_nodes.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_parameter_values.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_parametric_reforms.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_poverty.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_poverty_by_demographics.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_poverty_run.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_preservation_mirror.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_program_statistics.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_pyproject_pins.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_region.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_region_group_strategy.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_release_credentials.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_release_lock.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_release_manifests.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_release_tro_generation.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_results.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_run_record.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_scoping_strategy.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_small_follow_ups.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_spm_bundle_bootstrap.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_spm_bundle_tooling.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_spm_decile.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_spm_development_authentication.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_spm_household.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_spm_model.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_spm_selection.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_spm_selection_schema.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_trace_tro.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_trace_tro_verify.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_uk_analysis.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_uk_geography_assets.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_uk_geography_disclosure.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_uk_program_statistics.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_uk_regions.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_legacy_inputs.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_legacy_inputs_integration.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_legacy_spm.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_legacy_spm_integration.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_legacy_spm_model_contract.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_long_term_datasets.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_microsim_structural_reforms.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_native_alignment.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_native_hdf_weights.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_program_statistics.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_reform_application.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_regions.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_us_year_file_stamp.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_variable_labels.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_variable_metadata.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/test_zenodo_mirror.py +0 -0
- {policyengine-6.4.2 → policyengine-6.5.0}/tests/uk_contract_row_scenarios.py +0 -0
|
@@ -141,9 +141,10 @@ jobs:
|
|
|
141
141
|
run: policyengine bundle verify --country us --country uk --packages-only --json
|
|
142
142
|
Test:
|
|
143
143
|
runs-on: ubuntu-latest
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
|
|
144
|
+
# Observed 2026-10-14..15: 28 to 54 minutes per Python version, with runs on
|
|
145
|
+
# 3.12 and 3.13 cancelled at the old 60-minute limit. Fail a hung job instead
|
|
146
|
+
# of holding a shared org runner for the 6-hour default.
|
|
147
|
+
timeout-minutes: 90
|
|
147
148
|
strategy:
|
|
148
149
|
fail-fast: false
|
|
149
150
|
matrix:
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## [6.5.0] - 2026-10-15
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- Updated the UK Budget reform contract to version 1.1.0. Its rate-blending case, `mid_year_blended_rate`, now holds capital gains realisations fixed (both elasticity conventions zero and no separate Business Asset Disposal Relief elasticity), so its static expected values hold whatever realisation response policyengine-uk applies by default. Added `cgt_retention_response`, which checks the response itself: with a retention-rate elasticity of 1, a 20% basic rate from 6 April 2027 scales realised gains by (1 - 20%) / (1 - 18%) on every route. Added `cgt_default_response`, the same reform with no response settings, so the report shows the default response policyengine-uk applies; its checks expect the retention-rate elasticity of 1 that policyengine-uk#2396 makes the default, and report as pending (without gating) until policyengine.py pins that release. Each case also checks that all three response settings reached the model. `capital_gains_response()` builds a case's elasticity changes.
|
|
6
|
+
- Added a record of the capital gains realisation response to UK runs (`policyengine.tax_benefit_models.uk.capital_gains_responses`). Each population run records the policyengine-uk release and where it was installed from when that is not a package index, the caller's explicit `gov.simulation.capital_gains_responses` settings in the order they apply and, where the installed policyengine-uk reports it, the response it resolved after every reform and modifier. The record is kept with the output dataset (`metadata`), in the release bundle, the run record and the saved output file; household results carry it under `provenance`, and managed microsimulations under `policyengine_bundle["capital_gains_responses"]` for each year. UK cache and saved-output identity now includes a digest of the explicit settings, and a saved UK output is reused only under the same settings and the same policyengine-uk release from the same source (never from a local directory install). Each saved UK output is therefore calculated once more after upgrading, so a job that relies on a warm UK cache takes as long as a cold run the first time. Added `StaleOutputError`, a `ValueError` that `load()` raises for a saved output it refuses (UK and US alike); `Simulation.ensure()` logs the reason at INFO and runs the simulation again, instead of logging a warning with a traceback.
|
|
7
|
+
- Add execution receipt schema v1 (`policyengine.provenance.receipt`), which records exactly what produced a UK result. It records the installed wrapper, model and core versions, with wheel hashes only when the builder verified them against the wheel RECORD, plus the data release and the SHA-256 of the simulated dataset bytes. The certificate digest is attached only when the certificate covers that model, core, dataset and wrapper. The receipt also carries reform and baseline digests, which refuse modifiers they cannot bind, and the fiscal-year convention. Digests use RFC 8785 canonical JSON over strict JSON values (`policyengine.provenance.canonical`), and the JSON Schema ships at `policyengine/data/schemas/execution_receipt.v1.schema.json`.
|
|
8
|
+
- Added `composed_reform_identity` and the `policyengine-uk.scenario_composition/v1` representation, so an execution receipt can bind several reforms applied in one run, in order.
|
|
9
|
+
- Execution receipt reform digests accept infinite parameter values (an uncapped threshold is `.inf`), encoding `inf` and `-inf` as `{"$nonfinite": "Infinity"}` and `{"$nonfinite": "-Infinity"}`. A payload that uses the key `$nonfinite` itself is refused, and finite payloads digest as before.
|
|
10
|
+
- Budget reform contract case rows now carry `reform_identity`, the reform as a receipt binds it (`ReformIdentity`, named by the case id), for the representation each route consumes, or `null` with `reform_identity_unavailable` where receipt schema v1 cannot bind it; property, population and run-level rows carry `null` with the reason.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The pull-request Test job may run for 90 minutes instead of 60. It now takes 30 to 54 minutes per Python version, and runs on 3.12 and 3.13 were cancelled at the 60-minute limit.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Receipts no longer refuse a process started within a second of an install: on Linux, psutil's process start time can read about a second early, so install metadata written just before the start looked newer than the process. Metadata written more than two seconds after the reported start is still refused. An install within two seconds after the reported start is therefore not detected.
|
|
19
|
+
- Re-locked `uv.lock` with the pinned uv 0.12.14. Two dependency markers written by another uv version made the release's version bump rewrite the lock, which the reviewed-lock check rejects, so no release has published since #571.
|
|
20
|
+
- The UK Budget reform contract's shared-parameter check now catches list-valued parameters changed in place and changes to node or scale metadata or a scale's band count.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## [6.4.3] - 2026-10-15
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- The UK Budget reform contract's Universal Credit allowance check is tested on both runs: each must hold finite values and keep every payment at or above the payable floor, to within a penny. Beyond the payable floor, the check does not bound how far deductions rise: policyengine-uk scales every deduction type with the standard allowance, while the law holds some, such as advance recovery, fixed (policyengine-uk#2410).
|
|
28
|
+
|
|
29
|
+
|
|
1
30
|
## [6.4.2] - 2026-10-15
|
|
2
31
|
|
|
3
32
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: policyengine
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.5.0
|
|
4
4
|
Summary: A package to conduct policy analysis using PolicyEngine tax-benefit models.
|
|
5
5
|
Author-email: PolicyEngine <hello@policyengine.org>
|
|
6
6
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
@@ -672,7 +672,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
672
672
|
Requires-Python: <3.15,>=3.11
|
|
673
673
|
Description-Content-Type: text/markdown
|
|
674
674
|
License-File: LICENSE
|
|
675
|
-
Requires-Dist: pydantic>=2.
|
|
675
|
+
Requires-Dist: pydantic>=2.10.0
|
|
676
676
|
Requires-Dist: pandas>=2.0.0
|
|
677
677
|
Requires-Dist: h5py>=3.0.0
|
|
678
678
|
Requires-Dist: microdf_python<1.4,>=1.3.0
|
|
@@ -711,6 +711,7 @@ Requires-Dist: towncrier>=24.8.0; extra == "dev"
|
|
|
711
711
|
Requires-Dist: mypy>=1.11.0; extra == "dev"
|
|
712
712
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
713
713
|
Requires-Dist: hypothesis>=6.100.0; extra == "dev"
|
|
714
|
+
Requires-Dist: rfc8785>=0.1.4; extra == "dev"
|
|
714
715
|
Requires-Dist: policyengine-core==3.33.0; extra == "dev"
|
|
715
716
|
Requires-Dist: policyengine-us==2.2.1; extra == "dev"
|
|
716
717
|
Requires-Dist: policyengine-uk==2.130.1; extra == "dev"
|
|
@@ -106,6 +106,6 @@ src/policyengine/
|
|
|
106
106
|
|
|
107
107
|
**Country-specific model classes**: `PolicyEngineUSLatest` and `PolicyEngineUKLatest` inherit from a shared `MicrosimulationModelVersion` base (variable/parameter loading, manifest certification, `save`/`load`). Each subclass only implements `run()` and a handful of country hooks (`_load_system`, `_load_region_registry`, `_dataset_class`, `_get_runtime_data_build_metadata`). The US `run` applies reforms as a dict at `Microsimulation(reform=...)` construction time; the UK `run` wraps inputs as `UKSingleYearDataset` and applies reforms via a modifier after construction.
|
|
108
108
|
|
|
109
|
-
**LRU cache + file caching**: `Simulation.ensure()` checks an in-process LRU cache (max 100 entries), then tries loading from disk, then falls back to `run()` + `save()`.
|
|
109
|
+
**LRU cache + file caching**: `Simulation.ensure()` checks an in-process LRU cache (max 100 entries), then tries loading from disk, then falls back to `run()` + `save()`. When `load()` refuses an out-of-date saved output it raises `StaleOutputError` (a `ValueError`), which `ensure()` logs at INFO as an expected rerun. Any other load error is logged as a warning.
|
|
110
110
|
|
|
111
111
|
**Output pattern**: All output types inherit from `Output`, implement `.run()`, and populate result fields. Convenience functions (e.g., `calculate_us_poverty_rates()`) create, run, and return collections of output objects.
|
|
@@ -165,6 +165,8 @@ For UK households, a `YYYY-MM-DD` key is effective from that date until the next
|
|
|
165
165
|
|
|
166
166
|
UK households reject bare years such as `"2027"` or `2027`, because the underlying packages assign different meanings to them. Use `"2027-04-06.2028-04-05"` for one UK fiscal year or `"2027-01-01"` for an effective date. See [Reforms](reforms.md) for the shared `Simulation(policy=...)` and `Simulation(dynamic=...)` date contract. The US household route retains its country package's period-key handling.
|
|
167
167
|
|
|
168
|
+
UK household results record the capital gains realisation response the calculation applied, under `result["provenance"]["capital_gains_responses"]`: the policyengine-uk release, the reform's explicit `gov.simulation.capital_gains_responses` settings, and the assumption policyengine-uk resolved (see [The UK capital gains realisation response](microsim.md#the-uk-capital-gains-realisation-response)). `result.write(...)` includes it.
|
|
169
|
+
|
|
168
170
|
Structural reforms (new variables, formula swaps) require the `Simulation` path — see [Reforms](reforms.md).
|
|
169
171
|
|
|
170
172
|
## Year
|
|
@@ -30,7 +30,7 @@ total_snap.run()
|
|
|
30
30
|
total_snap.result
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
`Simulation.ensure()` loads a cached result if one exists, or runs and caches on miss. Call `Simulation.run()` explicitly if you want to bypass the cache.
|
|
33
|
+
`Simulation.ensure()` loads a cached result if one exists, or runs and caches on miss. Call `Simulation.run()` explicitly if you want to bypass the cache. A UK simulation's explicit capital gains response settings are part of its cache and saved-output identity; see [The UK capital gains realisation response](#the-uk-capital-gains-realisation-response).
|
|
34
34
|
|
|
35
35
|
### Canonical US SPM settings and provenance
|
|
36
36
|
|
|
@@ -288,6 +288,51 @@ checked. A saved UK simulation output records the data year its run anchored
|
|
|
288
288
|
on; `Simulation.load()` refuses an output saved without one, and
|
|
289
289
|
`Simulation.ensure()` runs it again.
|
|
290
290
|
|
|
291
|
+
### The UK capital gains realisation response
|
|
292
|
+
|
|
293
|
+
policyengine-uk scales realised capital gains in response to a reform, at an
|
|
294
|
+
elasticity set under `gov.simulation.capital_gains_responses`. Its default is
|
|
295
|
+
the model's, so it can change between policyengine-uk releases. Each UK run
|
|
296
|
+
records the response it applied, read after every reform and modifier.
|
|
297
|
+
|
|
298
|
+
- **Where it is kept.** It is in `simulation.release_bundle["capital_gains_responses"]`,
|
|
299
|
+
the run record's `results.json` and the saved output file.
|
|
300
|
+
- **What it holds.**
|
|
301
|
+
- `model`: the policyengine-uk release. Its `source` is `null` for a
|
|
302
|
+
release from a package index; otherwise it says where the install came
|
|
303
|
+
from: a version control commit, an archive's SHA-256, or a local
|
|
304
|
+
directory and whether it is editable.
|
|
305
|
+
- `request`: the caller's explicit settings under that path, in the order
|
|
306
|
+
they apply (policy, then dynamic), and their digest.
|
|
307
|
+
- `resolved`: the assumption the release reports. That is the convention,
|
|
308
|
+
the elasticities for main-rate gains and for gains qualifying for Business
|
|
309
|
+
Asset Disposal Relief, the MTR elasticity, the separate relief switch, and
|
|
310
|
+
whether each is the default, an override or set to zero for an MTR
|
|
311
|
+
elasticity. It is `null` with a release that doesn't report it.
|
|
312
|
+
- **Modifiers.** Changes a simulation modifier makes aren't listed under
|
|
313
|
+
`request`, as run records don't describe modifiers. `resolved` reflects them.
|
|
314
|
+
Neither the `storage_id` nor `Simulation.load()` sees them, so two
|
|
315
|
+
simulations with the same `id` whose modifiers set the response differently
|
|
316
|
+
share a saved output. Give such simulations different ids.
|
|
317
|
+
- **Identity.** A simulation with explicit settings adds `-cgr-<digest>` to its
|
|
318
|
+
`storage_id`, so its cache entry and saved output are kept apart from runs
|
|
319
|
+
with other settings. The digest covers each setting as given, including
|
|
320
|
+
whether it came from the policy or the dynamic and its dates. So the same
|
|
321
|
+
value set another way, or with an explicit end date instead of none, is
|
|
322
|
+
saved separately.
|
|
323
|
+
- **Reuse.** `Simulation.load()` reuses a saved UK output only when its record
|
|
324
|
+
shows the same policyengine-uk release, installed from the same source, and
|
|
325
|
+
the same settings. A branch installed from git or a local directory can
|
|
326
|
+
carry the version number of a published release with another default.
|
|
327
|
+
With policyengine-uk installed from a local directory, editable or not, no
|
|
328
|
+
saved UK output is reused, as its code can change without its version
|
|
329
|
+
changing.
|
|
330
|
+
- **Reruns.** Otherwise `load()` raises `StaleOutputError`, and
|
|
331
|
+
`Simulation.ensure()` runs the simulation again and logs the reason at INFO.
|
|
332
|
+
Outputs saved before outputs recorded the response are calculated once
|
|
333
|
+
more, so a job that relies on a warm UK cache takes as long as a cold run
|
|
334
|
+
the first time it runs after the upgrade.
|
|
335
|
+
|
|
291
336
|
## Simulations
|
|
292
337
|
|
|
293
338
|
A `Simulation` needs a dataset, a tax-benefit model version, and optionally a policy (reform):
|
|
@@ -379,6 +424,14 @@ Pass `allow_unmanaged=True` with a custom `dataset=` to opt out of the release
|
|
|
379
424
|
bundle. Explicit local paths and Hugging Face URIs remain supported in this
|
|
380
425
|
mode. GCS dataset URIs are not supported.
|
|
381
426
|
|
|
427
|
+
A UK managed microsimulation also records, in
|
|
428
|
+
`sim.policyengine_bundle["capital_gains_responses"]`, the capital gains
|
|
429
|
+
realisation response it applies in each year of its dataset (see
|
|
430
|
+
[The UK capital gains realisation response](#the-uk-capital-gains-realisation-response)).
|
|
431
|
+
Its `reform=` goes to policyengine-uk unchanged, so no request is listed.
|
|
432
|
+
Settings that conflict are recorded as an error for that year, and raise when
|
|
433
|
+
the response is calculated.
|
|
434
|
+
|
|
382
435
|
For managed simulations, `sim.policyengine_bundle` records the actual source
|
|
383
436
|
package, repository type, revision, verified SHA-256, and local path.
|
|
384
437
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Execution receipts
|
|
2
|
+
|
|
3
|
+
An execution receipt travels with a published number, such as a dashboard row, a briefing figure or an API response. It names everything that determined that number, so a reader can tell exactly which model, data and certificate produced it, and two numbers can be compared knowing whether they came from the same run.
|
|
4
|
+
|
|
5
|
+
Schema v1 covers UK results and is defined by `policyengine.provenance.receipt.ExecutionReceipt`. The JSON Schema ships in the wheel at `policyengine/data/schemas/execution_receipt.v1.schema.json`, and the same file is in the repository at [`src/policyengine/data/schemas/execution_receipt.v1.schema.json`](https://github.com/PolicyEngine/policyengine.py/blob/main/src/policyengine/data/schemas/execution_receipt.v1.schema.json).
|
|
6
|
+
|
|
7
|
+
## What a receipt records
|
|
8
|
+
|
|
9
|
+
| Field | Contents |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `wrapper`, `model`, `core` | The `policyengine`, `policyengine-uk` and `policyengine-core` versions that actually ran, read from the installed distributions rather than from the bundle manifest. `install_origin` records a PEP 610 direct URL as `archive`, `directory`, `editable` or `vcs`; it is absent for an ordinary index install with no `direct_url.json`. `wheel_sha256` appears only when the receipt builder verified it. One way is `wheel_paths=` (evidence `installed_record_verified`): the installed files, and any bytecode cache the interpreter would load instead of a source file, are checked against that wheel's RECORD. The other is the installer's recorded archive hash (`installer_archive_hash`), taken only when the install came from a `.whl` URL; a source archive's hash is never recorded as a wheel hash. |
|
|
12
|
+
| `data` | The data release and build, and the model version the data was built with (`build_model_version`, distinct from `model.version`). Also the dataset's logical name, URI, revision and the SHA-256 of the bytes that were simulated. The dataset is resolved exactly as `materialize_dataset` resolves it (overlays, per-dataset repository, package and revision, and the release manifest revision when an artifact has none of its own), and the bytes must be the ones the bundle pins for it: a receipt never gives an artifact's name to other bytes. |
|
|
13
|
+
| `certification_status`, `certificate` | `certified` only when the bundle certifies all of: the installed model and core versions, that dataset name and those bytes, and the running `policyengine` version. Each package must also have an absent or `archive` install origin, or a `directory` or `vcs` origin with `installed_record_verified` wheel evidence. A directory or VCS install without that evidence produces an `uncertified` receipt, which carries no certificate. An editable install is never certified, whatever the evidence: its wheel holds a pointer to a source tree (a `.pth` file or an import hook) rather than the source, so checking the installed files against it does not bind the code that ran. `certificate_source` records whether the builder read the certificate from the installed bundle (`packaged`) or from a manifest the caller passed (`caller_supplied`). It is the producer's statement, not evidence: trust a certificate only after `verify_certificate` (below). With the packaged bundle, the composition fingerprint of its TRACE TRO is recorded as well. |
|
|
14
|
+
| `reform`, `baseline` | Both are required. Each is `current_law` or a digest of the reform bound to its representation (see below). |
|
|
15
|
+
| `fiscal_year` | What a year label means: year 2027 is the fiscal year from 6 April 2027 to 5 April 2028, shown as `2027-28`. v1 allows only this convention. |
|
|
16
|
+
| `years` | The reported years, sorted and unique. |
|
|
17
|
+
| `supported_years` | The years the installed model projects the dataset to, from the dataset's own year (policyengine-uk's `extend_single_year_dataset`, without running a formula; the same rule as the qualification `supported_years` gate). A reported year outside them is unsupported: policyengine-core fills it by carrying or uprating the latest inputs, so its results are not evidence. `unsupported_years` lists them. The certified 1.58.0 dataset projects to 2024–2030, so a 2031-32 figure on it is unsupported. |
|
|
18
|
+
| `outputs_sha256` | Optional digest of the output payload the receipt binds. |
|
|
19
|
+
| `runtime` | Python version, platform and CI run URL. It is for diagnosis and is part of no digest. |
|
|
20
|
+
|
|
21
|
+
Malformed `direct_url.json` is refused because the builder cannot determine the package's install origin.
|
|
22
|
+
|
|
23
|
+
For each loaded top-level package (`policyengine`, `policyengine_uk` and `policyengine_core`), the builder also checks that its `__init__.py` comes from the installed distribution it names. Index, archive, VCS and non-editable directory installs must load the package file listed in that distribution's RECORD, resolved through `locate_file`. Only editable installs (`dir_info.editable: true`) must load from under the directory their PEP 610 URL names, including a `src/` layout. Paths are compared after resolving symlinks. A checkout ahead of a copied install on `PYTHONPATH` is refused because the receipt cannot name the code that ran. An unloaded package has no source path to compare; the version checks still apply.
|
|
24
|
+
|
|
25
|
+
## Building one
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from policyengine.provenance import materialize_dataset
|
|
29
|
+
from policyengine.provenance.receipt import build_execution_receipt, reform_identity
|
|
30
|
+
|
|
31
|
+
source = materialize_dataset("uk") # the certified release artifact, hash-verified
|
|
32
|
+
reform = {"gov.hmrc.income_tax.rates.uk[0].rate": {"2027-04-06": 0.21}}
|
|
33
|
+
|
|
34
|
+
receipt = build_execution_receipt(
|
|
35
|
+
country_id="uk",
|
|
36
|
+
dataset_path=source.path,
|
|
37
|
+
years=[2027, 2028],
|
|
38
|
+
reform=reform_identity(
|
|
39
|
+
reform, "policyengine.reform_dict/v1", name="basic rate 21p"
|
|
40
|
+
),
|
|
41
|
+
baseline=reform_identity(None),
|
|
42
|
+
outputs={"budgetary_impact": {"2027": 6.1e9, "2028": 6.4e9}},
|
|
43
|
+
)
|
|
44
|
+
print(receipt.certification_status, receipt.execution_identity_sha256)
|
|
45
|
+
receipt_json = receipt.to_canonical_json()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For a country `Microsimulation` built with `managed_microsimulation()`, call `build_execution_receipt_for_microsimulation(sim, years=..., reform=..., baseline=...)`. It reads the dataset that was loaded from `sim.policyengine_bundle`. The file must still hash to the SHA-256 verified when it was loaded, and the receipt must name the dataset, URI, revision and package recorded then; otherwise it is refused, because it would describe bytes that did not run. A simulation loaded from an unmanaged file or URI recorded no load-time hash, so it cannot be receipted. The microsimulation does not record its reform, so pass the reform you built it with.
|
|
49
|
+
|
|
50
|
+
There are two ways to bind a policyengine-uk `Scenario`:
|
|
51
|
+
- A scenario built from `parameter_changes` is bound by content with `scenario_reform_identity(scenario)`.
|
|
52
|
+
- `Scenario.from_reform(d)` wraps `d` in a closure that cannot be bound by its code, so bind `d` itself with `reform_identity(d, "policyengine-uk.reform_dict/v1")`.
|
|
53
|
+
|
|
54
|
+
A scenario with a module-level `simulation_modifier` needs `code_revision={"repository": ..., "git_sha": <full commit sha>}`. The identity then says `binding: "code_revision"` and binds:
|
|
55
|
+
- the function's module and qualified name;
|
|
56
|
+
- the SHA-256 of its source and of its module's source file;
|
|
57
|
+
- the default arguments of every function in that module, encoded with their types and floats by their bits (`float.hex()`, which writes infinities as `inf` and `-inf`; NaN is refused), so `1` and `1.0`, `0.0` and `-0.0`, a tuple and a list, or `None` and a missing key never share a digest (defaults with no exact encoding, such as objects, are refused, and so are two defaults that are one mutable object, such as one list set as both `first` and `second`, since the encoding cannot tell that from two equal lists);
|
|
58
|
+
- that revision.
|
|
59
|
+
|
|
60
|
+
None of these depends on where the code is checked out or which Python version runs it, so CI, a laptop and the server compute the same digest for the same code.
|
|
61
|
+
|
|
62
|
+
Build the receipt after the run. An identity returned by `scenario_reform_identity` keeps a private handle to the same scenario and code revision. Both receipt builders recompute `reform` and `baseline` identities with handles, and refuse if recomputing is refused or an identity member differs. The handle is excluded from equality, serialization, schemas and digests. A serialized identity loaded back has no handle and is not re-checked; its producer must ensure it describes the run. These checks take snapshots at identity time and receipt build time, so they cannot describe later changes.
|
|
63
|
+
|
|
64
|
+
**The modifier's module.** A digest can only name what the module's source fixes, so the module holding a bound modifier must have a shape whose state that source determines. At module level it may hold only:
|
|
65
|
+
- docstrings and `pass`;
|
|
66
|
+
- imports;
|
|
67
|
+
- assignments of a literal to one name, annotated or not (`RATE = 0.21`, `YEARS: tuple = (2026, 2027)`);
|
|
68
|
+
- undecorated `def` statements, none named like a dunder, whose type parameters (`def pennies[T](amounts: T)`) have no bound, constraints or default;
|
|
69
|
+
- an `if __name__ == "__main__":` block with no `else` (it never runs on import, so what it binds is ignored).
|
|
70
|
+
|
|
71
|
+
Each name is bound once. Defaults hold no `:=`, lambda, comprehension, `await` or `yield`. Annotations allow names, attributes, subscripts, constants, directly signed numeric constants and `X | Y`, with no calls. Anything else is refused, naming the line. That covers:
|
|
72
|
+
- a type parameter's bound, constraints or default (`def pennies[T: list[Decimal | float]]`), which Python evaluates only when something such as Pydantic first reads it, so typing's order-insensitive alias cache can decide what it holds;
|
|
73
|
+
- conditional, repeated or unpacked bindings;
|
|
74
|
+
- objects built at import (`PAYMENT = partial(pay, amount=120)`, whose captured values can change without the source changing);
|
|
75
|
+
- values computed from the environment, a file or the clock;
|
|
76
|
+
- decorators, class definitions, and code run at import.
|
|
77
|
+
|
|
78
|
+
The accepted module shape also refuses these constructs:
|
|
79
|
+
- arithmetic on literals, such as `WEEKLY = 520 / 52` or `MILLION = 10 ** 6`: use the computed literal (`WEEKLY = 10.0`, `MILLION = 1000000`), or compute the value inside a function;
|
|
80
|
+
- `if TYPE_CHECKING:` blocks: put type-only imports in a `.pyi` file or inside a function;
|
|
81
|
+
- `type` aliases, such as `type Rate = float`: use the concrete type in annotations, or put the alias in a `.pyi` file;
|
|
82
|
+
- starred annotations such as `*args: *Ts`: use a concrete argument annotation or `ParamSpec`'s `P.args` and `P.kwargs`.
|
|
83
|
+
|
|
84
|
+
Anywhere in the module, code may not:
|
|
85
|
+
- tell equal values apart by identity: `is` is allowed only against `None`, `True`, `False`, `...` or `type(...)`, and `type` may not be rebound. The compiler may make equal immutable constants one object, and a digest of values cannot tell one object from two equal ones.
|
|
86
|
+
- use `id`, `operator.is_` or `is_not`, `globals`, `vars`, `locals`, `exec`, `eval`, `compile`, `__import__` or `breakpoint`, however imported, or import `builtins`;
|
|
87
|
+
- read annotation values through `typing.get_type_hints`, `inspect.signature`, `inspect.getfullargspec`, `get_annotations` or `from_callable`; these explicit metadata readers remain outside the accepted module shape.
|
|
88
|
+
- declare a name `global`.
|
|
89
|
+
- read dunder attributes other than `__init__` and `__name__`, or dunder names other than `__name__` and `__file__`. Reading a function's or module's own metadata is introspection.
|
|
90
|
+
- use `getattr`, `hasattr`, `setattr` or `delattr` with a non-literal attribute name, or a literal dunder other than `__init__` and `__name__`.
|
|
91
|
+
|
|
92
|
+
Put measure values in literal constants or in default arguments.
|
|
93
|
+
|
|
94
|
+
**What was imported.** A digest must only name code that ran, so at identity time, and again when a receipt is built with that identity's private handle, the module's source as read now is compiled and checked against what was imported, ignoring file names and line numbers:
|
|
95
|
+
- **Versions.** At receipt build time, any recorded version of policyengine, policyengine-uk or policyengine-core must still match its installed version. Independently, each package's distribution metadata must not have been written after this process started. The builder compares the latest modification or change time of its `.dist-info` directory and of the existing `METADATA`, `RECORD`, `INSTALLER` and `direct_url.json` files against `psutil.Process().create_time()`, and refuses metadata written more than two seconds after it. On Linux psutil builds that start time from the boot time in whole seconds plus the start offset in clock ticks, so it can read up to about a second early (one second plus one tick). Without the margin, a process started just after an install finished would be refused. The margin is a blind window: metadata written within two seconds after the reported start is accepted, so an install that lands in that window is not detected. Past it, metadata is refused regardless of import order or whether a version was recorded; an unavailable metadata location is refused too. Restart the process and run again. This also refuses installing a package and then importing it in the same process, such as a notebook `pip install` without a restart, even when that import loaded the new code, unless the install finishes within two seconds of the process starting.
|
|
96
|
+
- **Functions.** The source is compiled as a whole, so each function compiles in its module's context, imports included. The modifier and every other function defined in that file must use the module's own globals and match that code, nested code and constants included. Constants are compared by exact type, by IEEE 754 bytes (so a NaN's sign counts) and by how they share objects. Annotation functions that Python 3.14 compiles (`__annotate__`) are the compiler's, not the module's, and a generic function (`def helper[T](...)`, Python 3.12+) is checked through the wrapper the compiler makes to bind its type parameters.
|
|
97
|
+
- **Function names.** A `def` name must still be bound to that function, not to another function of the same file or any other object.
|
|
98
|
+
- **Code inside functions.** It runs after import, so a type it builds is made then: an expression in a function body, or the annotations, type-parameter bounds and class annotations of a helper defined there (quoted or not). typing caches its generic aliases (`List`, `Dict`, `Optional`, `Annotated`) and user generics (`Generic` subclasses, Pydantic models) by argument equality, and a union equals one holding its members in any order. So `List[Union[Decimal, float]]` built there can return an alias cached for `List[Union[float, Decimal]]`, and Pydantic tries a union's members in order. A function whose code subscripts such a generic with a union or `Literal` is refused. Builtin generics, `collections.abc` classes and classes whose `__class_getitem__` is implemented in C (NumPy's `ndarray` and `dtype`) build a new alias in source order and are allowed (`list[Decimal | float]`), as is a bare `Union[Decimal, float]`. Resolution is conservative. A name stands for every object the module, or any import anywhere inside the function, binds it to, so a helper importing `typing.Sequence` is caught even when a sibling imports `collections.abc.Sequence`. Every string in the function's code that parses as an expression is inspected as a type, since a forward reference such as `Optional['List[Union[Decimal, float]]']` is evaluated when something resolves it. `Literal` values and `Annotated` metadata are left as strings. Local aliases of typing objects (`L = List`) are not followed. The module's own function signatures are compared against their source instead (below).
|
|
99
|
+
- **Function metadata.** Each function's name, qualified name and docstring must still be what its `def` made, compared as the compiler stores them. A generic function's `__type_params__` must still be bare type parameters of the names and kinds its source declares, in order, with no bound, constraints, default or declared variance. Each annotation must still hold what its source names, resolved against the module's globals and builtins (and those type parameters): named objects are compared by identity; reconstructed generics, unions and `ParamSpec` argument proxies must have the same origin and arguments, recursively and in order. Source arguments are resolved separately so typing's cache cannot hide a reordered union. Parameterised typing aliases are refused when their source order cannot be checked independently of that cache. Subscripted annotations that do not expose a typing origin and arguments, such as generated Pydantic model classes, are also refused. Quoted annotations and strings stored by `from __future__ import annotations` must match their source, and their resolvable type expressions must also match source order. This includes annotations consumed implicitly by libraries such as `functools.singledispatch`. It must use the builtins module and carry no other attributes (`helper.amount = 240` is state).
|
|
100
|
+
- **Defaults.** Literal default arguments in the source, such as `rate=0.21`, must still be the function's defaults, with the same type.
|
|
101
|
+
- **Literal constants.** Every literal constant must still hold its value, compared exactly: the same types throughout, floats by their bits, dictionary keys in order, for every literal kind including bytes, sets and complex numbers.
|
|
102
|
+
- **No shared mutable objects.** No two literal constants or defaults may be one mutable object, which a value check cannot tell from two equal objects.
|
|
103
|
+
- **Imports.** Every imported name must still be what its import binds: `from math import floor` must still be `math.floor`, and `import math` the loaded `math` module. It must hold a module or a callable other than a `functools.partial`; `from __future__` features are the exception. An imported value such as `from params import AMOUNT` is refused even when unused, since code can read a name without naming it. For the same reason, a star import of a module that exports data values is refused; import the callables you use by name. Each name a star import provides must be that module's own.
|
|
104
|
+
- **No other names.** The namespace may hold no name the source does not bind, other than the dunders the import system and annotations set. The module's docstring and builtins must be what importing it made. A name set on the module after import is refused however the code would read it.
|
|
105
|
+
|
|
106
|
+
A measures file edited after import, or a constant, default, import or function reassigned before either snapshot, is refused. If it changes after identity time, a receipt built with the identity's handle refuses the changed modifier, naming the difference or the reason it can no longer be bound. The check does not cover state in other modules (such as `params.PAYMENT` read through `import params`), or data the code reads at run time. Code in other modules is bound by `code_revision` or the installed versions, and `code_revision` names the code, not the process. Receipts are claims by their producer: these checks protect an honest producer against mistakes, not a reader against a producer who builds code to mislead. Code that reaches into frames, the garbage collector or other modules' namespaces is beyond them.
|
|
107
|
+
|
|
108
|
+
Some modifiers are refused outright because their behaviour depends on values the digest cannot bind: closures, lambdas, nested functions, methods, `functools.partial` and builtins. Anything that is not a `Scenario` is refused too, rather than read as current law.
|
|
109
|
+
|
|
110
|
+
### Several reforms in one run
|
|
111
|
+
|
|
112
|
+
A combined run, such as a whole Budget package, applies several measures in one simulation. Bind each measure on its own, then bind the package with `composed_reform_identity(members, applied_before_data_load=...)`, passing the members' identities in application order and the phase the combined `Scenario` runs in. Its representation is `policyengine-uk.scenario_composition/v1`, and its digest covers the phase and each member's representation, digest and binding, in that order.
|
|
113
|
+
|
|
114
|
+
The representation means one simulation, in the stated phase, that applies every member's parameter changes and then runs each member's simulation modifier in member order, whatever the policyengine-uk version. That order is part of the payload (`"application": "parameter_changes_then_modifiers"`). The changes are applied where the engine applies a Scenario's own parameter changes, so each member's changes act as they do when that member runs alone. The composer must run exactly that. It is not `Scenario.__add__`: that merge depends on how members are grouped, and it leaves the order to the engine (policyengine-uk 2.125.1 runs a Scenario's modifier before its parameter changes).
|
|
115
|
+
|
|
116
|
+
On policyengine-uk 2.126.12, run a composition as one `Scenario` assembled from each member's own Scenario: `Scenario.from_reform(d)` for a reform dictionary member, the scenario itself for a scenario member. It carries the scenario members' merged `parameter_changes`, every member's `dated_reforms` in member order, and a modifier that runs every member's modifier in member order, including the one `from_reform` adds. Two shortcuts give a different run on that version:
|
|
117
|
+
- Merging a reform dictionary into `parameter_changes`. A scalar value in a reform dictionary applies from 2023; in `parameter_changes` it applies from 2000.
|
|
118
|
+
- Applying changes inside a modifier. When `applied_before_data_load` is false, that version applies a Scenario's own changes before the construction steps that read parameters (the Universal Credit rebalancing inputs and the structural reforms switched on by parameters) and runs its modifier after them, so a change made inside a modifier reaches neither step.
|
|
119
|
+
|
|
120
|
+
The rules that follow:
|
|
121
|
+
- The phase has no default. The composition's phase is the one that runs, whatever phase a member scenario had on its own.
|
|
122
|
+
- No two members may change the same parameter, in any form, not even to the same value. With disjoint changes the result does not depend on grouping or order. The composer must check this; `composed_reform_identity` sees only identities, so it cannot.
|
|
123
|
+
- Order is part of the digest: the same members in another order give a different digest. A nested composition's members are applied as if listed in its place.
|
|
124
|
+
- A baseline shared by several members (one that applies parameter changes only) appears once, at the position of the first member that shares it; being parameter changes, its position does not affect the run.
|
|
125
|
+
- Each member is revalidated, so an identity edited past its rules is refused rather than dropped or hashed.
|
|
126
|
+
- A member with a live re-check handle (from `scenario_reform_identity`, or a composition) is recomputed before composing and refused if it changed. The package's identity keeps a handle that recomposes from those members, so both receipt builders re-check every member, nested compositions included, after the run. An identity loaded from JSON has no handle, as for any identity.
|
|
127
|
+
- `current_law` members are dropped, and a composition with no remaining member is `current_law`. The binding is `code_revision` if any member's is.
|
|
128
|
+
|
|
129
|
+
A reader who holds the members' identities can recompute the composition's digest and compare.
|
|
130
|
+
|
|
131
|
+
To check a certificate, call `verify_certificate(receipt, bundle)` with the policyengine manifest for `certificate.bundle_version`. It rebuilds the certificate from that manifest's certification record, the same way the builder does. Then it compares every identity member: the record digest, the basis, the certified model, core and dataset digest, the certifier and the bundle version. It also checks that the receipt's whole `data` identity is the certified dataset with the certified bytes. An empty list means the bundle certifies exactly what the receipt claims, and through the receipt's own rules that pins `model.version`, `core.version` and `wrapper.version` too. `certificate_source`, `qualification_receipt_sha256`, `tro_composition_fingerprint` and wheel hashes are not determined by the bundle, so it does not check them.
|
|
132
|
+
|
|
133
|
+
Pass the release artifact that was loaded as `dataset_path`. A derived per-year file such as `enhanced_frs_2024_25_year_2026.h5` has different bytes from the artifact the bundle pins, so a receipt for it is refused.
|
|
134
|
+
|
|
135
|
+
## Digests
|
|
136
|
+
|
|
137
|
+
Every receipt digest is the lowercase hexadecimal SHA-256 of the [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) canonical JSON of a value, computed in two steps:
|
|
138
|
+
1. **Strict JSON conversion.** Only JSON-native values are accepted: strings, booleans, null, integers within ±(2^53−1), finite doubles, arrays and objects with string keys. NumPy boolean, integer and floating scalars are converted to their values, integer keys to their decimal strings, and `datetime.date` to `YYYY-MM-DD`. Anything else is refused rather than silently stringified, including sets, `Decimal`, arrays (0-d and masked ones too, so `np.ma.masked` never becomes `0.0`), NaN and colliding keys.
|
|
139
|
+
2. **Null removal.** Object members whose value is `null` are removed. Array elements are kept.
|
|
140
|
+
|
|
141
|
+
RFC 8785 fixes member order, string escaping and number formatting, so Python and JavaScript produce the same bytes. Removing nulls means a producer that writes `"field": null` and one that omits the field compute the same digest. In Python use `policyengine.provenance.receipt.content_sha256`. In JavaScript:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
import canonicalize from "canonicalize";
|
|
145
|
+
import { createHash } from "node:crypto";
|
|
146
|
+
|
|
147
|
+
const dropNulls = (v) =>
|
|
148
|
+
Array.isArray(v) ? v.map(dropNulls)
|
|
149
|
+
: v && typeof v === "object"
|
|
150
|
+
? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== null).map(([k, x]) => [k, dropNulls(x)]))
|
|
151
|
+
: v;
|
|
152
|
+
const digest = (value) => createHash("sha256").update(canonicalize(dropNulls(value)), "utf8").digest("hex");
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Reform digests are the exception: a reform payload is digested with its nulls kept, because there a null can mean something. `Scenario.from_reform({"p": {"2027": None}})` removes a value that `{"p": {}}` keeps, so the two must not share a digest. Reform payloads may also hold infinite numbers, which PolicyEngine parameters use for an uncapped threshold and JSON cannot hold. `inf` is digested as the object `{"$nonfinite": "Infinity"}` and `-inf` as `{"$nonfinite": "-Infinity"}`, and a payload that uses the key `$nonfinite` itself is refused, so no other payload digests like an infinity. The encoding happens inside strict conversion, so a payload it refuses without infinities, such as one with colliding keys, is refused with them too. NaN is refused, and a payload without infinities digests as before, except one whose strings, keys or dates are subclasses that override how `str` or `date` render: those now digest by their characters and their date, as content digests do. The JavaScript above covers content digests only: a JavaScript verifier of reform digests must keep nulls, replace `Infinity` and `-Infinity` with the reserved objects before canonicalizing, and refuse a payload with a `$nonfinite` key.
|
|
156
|
+
|
|
157
|
+
The pre-promotion replay interface with policyengine-scorecard follows a different rule, set by its own schemas: plain RFC 8785 over the document as sent, nulls included. `policyengine.provenance.canonical.canonical_sha256` computes that.
|
|
158
|
+
|
|
159
|
+
| Digest | Value hashed |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `execution_identity_sha256` | `schema_version`, `country_id`, `wrapper`, `model`, `core`, `data`, `certification_status`, and the certificate's content: bundle version, record digest, basis, certified versions, dataset digest and certifier. Results with the same execution identity recorded the same package identities (versions, install origins and wheel evidence) and the same data under the same certificate, however the receipt was built. For a certified receipt those identities name the code that ran. An uncertified one does not bind its packages' contents: two runs from an editable checkout edited in between share an execution identity. |
|
|
162
|
+
| `receipt_sha256` | The whole receipt except `runtime` and the reform and baseline `name` labels. Renaming a measure therefore does not change it. |
|
|
163
|
+
| `certificate.certificate_sha256` | The bundle manifest's `data_releases.uk` record. |
|
|
164
|
+
| `reform.reform_sha256` | `{"representation": ..., "payload": ...}`, with the payload exactly as given, nulls kept (strict JSON conversion with infinities as `{"$nonfinite": "Infinity"}` or `"-Infinity"`, then RFC 8785, with no null removal). For a composition the payload is `{"applied_before_data_load": ..., "application": "parameter_changes_then_modifiers", "members": [{"representation", "reform_sha256", "binding"}, ...]}`, with members in application order. |
|
|
165
|
+
| `outputs_sha256` | The output payload. |
|
|
166
|
+
|
|
167
|
+
Reform digests are never normalized across representations. The same keys can legitimately mean different things in different representations: for example, a bare year in a policyengine-uk country dictionary changes that one year only. So each digest is tied to the representation that was submitted. The v1 representations are `policyengine.parameter_values/v1`, `policyengine.reform_dict/v1`, `policyengine-uk.reform_dict/v1`, `policyengine-uk.scenario/v1`, `policyengine-uk.scenario_composition/v1` and `policyengine-sim-api.policy/v1`.
|
|
168
|
+
|
|
169
|
+
## Rules JSON Schema cannot express
|
|
170
|
+
|
|
171
|
+
Consumers that validate with the JSON Schema rather than the Python model must also check the rules listed under `x-policyengine-rules` in the schema file:
|
|
172
|
+
|
|
173
|
+
- `years` is sorted with no duplicates, and each year is written as an integer (`2027`, not `2027.0`);
|
|
174
|
+
- `supported_years` is sorted with no duplicates, and a year in `years` but not in `supported_years` must be shown as provisional or not at all;
|
|
175
|
+
- a certificate is present exactly when the receipt is certified, and it names the receipt's country, model and core versions, dataset digest and `policyengine` version;
|
|
176
|
+
- a `current_law` reform has no digest, while any other reform has both a representation and a digest;
|
|
177
|
+
- a wheel digest always comes with its evidence;
|
|
178
|
+
- a certified receipt has no `editable` package, and no `directory` or `vcs` package without `installed_record_verified` wheel evidence;
|
|
179
|
+
- a certificate is trusted only after it, and the receipt's `data`, have been checked against the published manifest it names (`verify_certificate`).
|
|
180
|
+
|
|
181
|
+
The schema itself fixes the package names (`policyengine`, `policyengine-uk`, `policyengine-core`) and the UK fiscal-year convention, which starts on 6 April.
|
|
182
|
+
|
|
183
|
+
## Compatibility
|
|
184
|
+
|
|
185
|
+
Adding an optional field is a compatible v1 change. Optional fields are omitted from digests when absent, so existing digests stay valid. Removing or redefining a field, or changing what a digest covers, requires schema v2.
|
|
@@ -46,7 +46,7 @@ The directory contains:
|
|
|
46
46
|
| `bundle.trace.tro.jsonld` | The certified bundle TRO (model + data pins) |
|
|
47
47
|
| `reform.json` | The reform as parameter values with effective dates |
|
|
48
48
|
| `input.json` | Input dataset hash, dynamics, scoping, extra variables |
|
|
49
|
-
| `results.json` | Output dataset hash and per-entity table summaries; for US runs, the SPM receipt and the renamed stored inputs mapped onto live ones (see [Renamed inputs in stored data](microsim.md#renamed-inputs-in-stored-data)) |
|
|
49
|
+
| `results.json` | Output dataset hash and per-entity table summaries; for US runs, the SPM receipt and the renamed stored inputs mapped onto live ones (see [Renamed inputs in stored data](microsim.md#renamed-inputs-in-stored-data)); for UK runs, the capital gains realisation response the run applied (see [The UK capital gains realisation response](microsim.md#the-uk-capital-gains-realisation-response)) |
|
|
50
50
|
|
|
51
51
|
All payload files are written with the same canonical JSON used for
|
|
52
52
|
hashing, so the record verifies offline exactly as written.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "policyengine"
|
|
7
|
-
version = "6.
|
|
7
|
+
version = "6.5.0"
|
|
8
8
|
description = "A package to conduct policy analysis using PolicyEngine tax-benefit models."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -19,7 +19,7 @@ classifiers = [
|
|
|
19
19
|
"Programming Language :: Python :: 3.14",
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
|
-
"pydantic>=2.
|
|
22
|
+
"pydantic>=2.10.0",
|
|
23
23
|
"pandas>=2.0.0",
|
|
24
24
|
"h5py>=3.0.0",
|
|
25
25
|
"microdf_python>=1.3.0,<1.4",
|
|
@@ -69,6 +69,7 @@ dev = [
|
|
|
69
69
|
"mypy>=1.11.0",
|
|
70
70
|
"pytest-cov>=5.0.0",
|
|
71
71
|
"hypothesis>=6.100.0",
|
|
72
|
+
"rfc8785>=0.1.4",
|
|
72
73
|
"policyengine-core==3.33.0",
|
|
73
74
|
"policyengine-us==2.2.1",
|
|
74
75
|
"policyengine-uk==2.130.1",
|
|
@@ -96,6 +97,9 @@ testpaths = [
|
|
|
96
97
|
filterwarnings = [
|
|
97
98
|
"ignore::pydantic.warnings.PydanticDeprecatedSince20",
|
|
98
99
|
]
|
|
100
|
+
markers = [
|
|
101
|
+
"real_install: runs the receipt's loaded-package check against the real installation",
|
|
102
|
+
]
|
|
99
103
|
|
|
100
104
|
[tool.ruff]
|
|
101
105
|
line-length = 88
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Write the published JSON Schemas for policyengine.py receipt contracts.
|
|
2
|
+
|
|
3
|
+
The Pydantic models in ``policyengine.provenance.receipt`` are the source;
|
|
4
|
+
the schema files under ``src/policyengine/data/schemas/`` are what other
|
|
5
|
+
languages consume. ``tests/test_execution_receipt.py`` checks that the two
|
|
6
|
+
accept and reject the same receipts.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
python scripts/generate_receipt_schemas.py # write
|
|
11
|
+
python scripts/generate_receipt_schemas.py --check # fail if stale
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
23
|
+
SCHEMA_DIR = REPO_ROOT / "src" / "policyengine" / "data" / "schemas"
|
|
24
|
+
os.environ.setdefault("POLICYENGINE_SKIP_COUNTRY_IMPORTS", "1")
|
|
25
|
+
sys.path.insert(0, str(REPO_ROOT / "src"))
|
|
26
|
+
|
|
27
|
+
from policyengine.provenance.receipt import ExecutionReceipt # noqa: E402
|
|
28
|
+
|
|
29
|
+
SCHEMAS = {
|
|
30
|
+
"execution_receipt.v1.schema.json": ExecutionReceipt.json_schema,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render(builder) -> str:
|
|
35
|
+
return json.dumps(builder(), indent=2, sort_keys=True, ensure_ascii=False) + "\n"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv: list[str] | None = None) -> int:
|
|
39
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
40
|
+
parser.add_argument("--check", action="store_true")
|
|
41
|
+
args = parser.parse_args(argv)
|
|
42
|
+
stale = []
|
|
43
|
+
for filename, builder in SCHEMAS.items():
|
|
44
|
+
path = SCHEMA_DIR / filename
|
|
45
|
+
content = render(builder)
|
|
46
|
+
if path.exists() and path.read_text() == content:
|
|
47
|
+
continue
|
|
48
|
+
if args.check:
|
|
49
|
+
stale.append(path)
|
|
50
|
+
continue
|
|
51
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
path.write_text(content)
|
|
53
|
+
print(f"wrote {path.relative_to(REPO_ROOT)}")
|
|
54
|
+
for path in stale:
|
|
55
|
+
print(f"{path.relative_to(REPO_ROOT)} is not up to date.", file=sys.stderr)
|
|
56
|
+
return 1 if stale else 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
raise SystemExit(main())
|
|
@@ -48,3 +48,11 @@ else: # pragma: no cover
|
|
|
48
48
|
uk = None # type: ignore[assignment]
|
|
49
49
|
|
|
50
50
|
__all__ = ["Simulation", "outputs", "uk", "us"]
|
|
51
|
+
|
|
52
|
+
# The wrapper's own version as imported, for receipts to compare with what is
|
|
53
|
+
# installed when they are built.
|
|
54
|
+
from policyengine.provenance.loaded import ( # noqa: E402
|
|
55
|
+
record_loaded_versions as _record_loaded_versions,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_record_loaded_versions(("policyengine",))
|
|
@@ -27,6 +27,7 @@ from .scoping_strategy import (
|
|
|
27
27
|
)
|
|
28
28
|
from .simulation import Simulation as Simulation
|
|
29
29
|
from .tax_benefit_model import TaxBenefitModel as TaxBenefitModel
|
|
30
|
+
from .tax_benefit_model_version import StaleOutputError as StaleOutputError
|
|
30
31
|
from .tax_benefit_model_version import (
|
|
31
32
|
TaxBenefitModelVersion as TaxBenefitModelVersion,
|
|
32
33
|
)
|
|
@@ -20,6 +20,7 @@ emitting a false certificate.
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
import copy
|
|
23
24
|
from dataclasses import dataclass, field
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import TYPE_CHECKING, Any, Mapping, Optional, Union
|
|
@@ -202,6 +203,29 @@ def build_simulation_run_record_payloads(
|
|
|
202
203
|
if legacy_spm_declarations is not None:
|
|
203
204
|
results_payload["legacy_spm_declarations"] = dict(legacy_spm_declarations)
|
|
204
205
|
|
|
206
|
+
# The capital gains realisation response a UK run applied: the model
|
|
207
|
+
# resolves its default, so the record binds the assumption itself. See
|
|
208
|
+
# ``policyengine.tax_benefit_models.uk.capital_gains_responses``.
|
|
209
|
+
capital_gains_responses = (
|
|
210
|
+
getattr(simulation.output_dataset, "metadata", None) or {}
|
|
211
|
+
).get("capital_gains_responses")
|
|
212
|
+
if capital_gains_responses is not None:
|
|
213
|
+
from policyengine.tax_benefit_models.uk.capital_gains_responses import (
|
|
214
|
+
simulation_request,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
capital_gains_responses["request"]["digest"]
|
|
219
|
+
!= simulation_request(simulation)["digest"]
|
|
220
|
+
):
|
|
221
|
+
raise ValueError(
|
|
222
|
+
"Capital gains response settings changed since this output was "
|
|
223
|
+
"calculated; run again before writing a run record"
|
|
224
|
+
)
|
|
225
|
+
results_payload["capital_gains_responses"] = copy.deepcopy(
|
|
226
|
+
capital_gains_responses
|
|
227
|
+
)
|
|
228
|
+
|
|
205
229
|
return {"reform": reform, "input": input_payload, "results": results_payload}
|
|
206
230
|
|
|
207
231
|
|