diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index ecd0558a..de5b4697 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -25,7 +25,8 @@ jobs: python-version: "3.13" activate-environment: true - name: Install - # #715 candidate UK 2.147.2/core 3.33.3; released certification pending. Retained + # d2011's UK 2.151.0/core 3.33.3 and the carrier the lock pins (policyengine.py 6.11.1, + # the pair's release; tests require provenance to certify the filled pair). Retained # historical pilot receipts keep their original model/data labels; # no test needs the private Enhanced FRS file. run: | diff --git a/README.md b/README.md index cb047918..734edfe5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This is a **provisional, model-conditional** release ahead of the 28 October 202 Headline, 2039-40 (Enhanced FRS, cash terms, UK): a **provisional, model-conditional expected saving of £11.1bn gross and £7.9bn net of tax and benefits**, the probability-weighted average over 50,000 simulated paths of prices and earnings centred on the OBR's forecast, estimated from 160 full runs (Monte Carlo error about ±£0.1bn). -**Data.** The published figures use the certified bundle, **policyengine.py 6.2.6** (policyengine-uk 2.125.1, policyengine-core 3.33.0) with **Enhanced FRS 1.58.0**. The same 40 paths are also run on **Microcosm's UK release** (`microcosm_uk_2024_25`, tag `microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68`, uncertified under this bundle) as a paired cross-check: Microcosm minus Enhanced FRS is +£1.12bn gross and +£0.49bn net in 2039-40. The cross-check is in `data/results.json` but not on the dashboard. This is the historical pre-Budget bundle. The binding rebuild will use the **pending d1727 release**, certifying policyengine-uk main around 26 October and **Enhanced FRS 1.58.2**, with separate model and data bridge steps before coverage and the fiscal run; see [REBUILD.md](docs/REBUILD.md). +**Data.** The published figures use the certified bundle, **policyengine.py 6.2.6** (policyengine-uk 2.125.1, policyengine-core 3.33.0) with **Enhanced FRS 1.58.0**. The same 40 paths are also run on **Microcosm's UK release** (`microcosm_uk_2024_25`, tag `microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68`, uncertified under this bundle) as a paired cross-check: Microcosm minus Enhanced FRS is +£1.12bn gross and +£0.49bn net in 2039-40. The cross-check is in `data/results.json` but not on the dashboard. This is the historical pre-Budget bundle. The binding rebuild will use d2011's pair, released in **policyengine.py 6.11.1** (policyengine-uk 2.151.0, policyengine-core 3.33.3): Microcosm's national cut `microcosm-uk-2024-25-national-20261025T091414Z-a6299504` as primary, with its local-area cut for constituency and local-authority runs, and **Enhanced FRS 1.58.2** as the paired sensitivity. Separate model and data bridge steps run before coverage and the fiscal run; see [REBUILD.md](docs/REBUILD.md). ## What the dashboard shows diff --git a/data/binding-bridge.json b/data/binding-bridge.json new file mode 100644 index 00000000..38de8681 --- /dev/null +++ b/data/binding-bridge.json @@ -0,0 +1,67 @@ +{ + "schema_version": 1, + "status": "ready", + "decision": "d2011 = A, Max 2026-10-25: the certified 2.151.0 Microcosm UK Budget pair (microcosm#1396) replaces #715's a133f37a pair; d1983 = B, Max 2026-10-24, put the binding primary on Microcosm", + "baseline_receipt": { + "path": "data/pilot/J-bridge-certified.json", + "sha256": "f5f42f5a6e20eb251dd26a283b840fdb825843e8edc9078c02fea78f14b6ac2f" + }, + "steps": { + "model": { + "from": { + "packages": { + "policyengine": "6.2.6", + "policyengine-uk": "2.125.1", + "policyengine-core": "3.33.0" + }, + "dataset": { + "name": "enhanced_frs_2024_25@1.58.0", + "sha256": "5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6" + } + }, + "to": { + "packages": { + "policyengine": "6.11.1", + "policyengine-uk": "2.151.0", + "policyengine-core": "3.33.3" + }, + "dataset": { + "name": "enhanced_frs_2024_25@1.58.0", + "sha256": "5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6" + } + } + }, + "data": { + "from": { + "packages": { + "policyengine": "6.11.1", + "policyengine-uk": "2.151.0", + "policyengine-core": "3.33.3" + }, + "dataset": { + "name": "enhanced_frs_2024_25@1.58.0", + "sha256": "5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6" + } + }, + "to": { + "packages": { + "policyengine": "6.11.1", + "policyengine-uk": "2.151.0", + "policyengine-core": "3.33.3" + }, + "dataset": { + "name": "microcosm_uk_2024_25@a6299504", + "sha256": "b96a8cd625b645a409c063b1de5158532708a2f0d3c3897a794a8fb0bbd5064e" + } + } + } + }, + "pending_release": { + "pull_request": "https://github.com/PolicyEngine/policyengine.py/pull/720", + "reviewed_head": "99a3e48b405e513a98850c64ee3564f0ca8e7553", + "bundle_manifest_path": "src/policyengine/data/bundle/manifest.json", + "policyengine_version": "6.11.1", + "bundle_manifest_sha256": "ab60903b390e7a765ea6558ec5e2d8fa5aede06b6f735fbf4e4c96109ffd1617", + "gate": "d2011 = A: the 2.151.0 national (microcosm#1396, a6299504) and its local-area cut certified together; fallback to #715's a133f37a pair with policyengine.py #699's area routing if not certified by 2026-10-27T18:00Z" + } +} diff --git a/data/binding-bridge.pending.json b/data/binding-bridge.pending.json index 6cdf3ff9..abdaa289 100644 --- a/data/binding-bridge.pending.json +++ b/data/binding-bridge.pending.json @@ -1,7 +1,7 @@ { "schema_version": 1, "status": "pending", - "decision": "d1983 = B, Max 2026-10-24; certified policyengine.py #715 Microcosm UK Budget pair supersedes d1727 qualification-ledger primary", + "decision": "d2011 = A, Max 2026-10-25: the certified 2.151.0 Microcosm UK Budget pair (microcosm#1396) replaces #715's a133f37a pair; d1983 = B, Max 2026-10-24, put the binding primary on Microcosm", "baseline_receipt": { "path": "data/pilot/J-bridge-certified.json", "sha256": "f5f42f5a6e20eb251dd26a283b840fdb825843e8edc9078c02fea78f14b6ac2f" @@ -22,7 +22,7 @@ "to": { "packages": { "policyengine": null, - "policyengine-uk": "2.147.2", + "policyengine-uk": "2.151.0", "policyengine-core": "3.33.3" }, "dataset": { @@ -35,7 +35,7 @@ "from": { "packages": { "policyengine": null, - "policyengine-uk": "2.147.2", + "policyengine-uk": "2.151.0", "policyengine-core": "3.33.3" }, "dataset": { @@ -46,22 +46,22 @@ "to": { "packages": { "policyengine": null, - "policyengine-uk": "2.147.2", + "policyengine-uk": "2.151.0", "policyengine-core": "3.33.3" }, "dataset": { - "name": "microcosm_uk_2024_25@a133f37a", - "sha256": "64f83900ca3a1242fb017585234844ce89f743d32e41cd2b6558057777f914aa" + "name": "microcosm_uk_2024_25@a6299504", + "sha256": "b96a8cd625b645a409c063b1de5158532708a2f0d3c3897a794a8fb0bbd5064e" } } } }, "pending_release": { - "pull_request": "https://github.com/PolicyEngine/policyengine.py/pull/715", - "reviewed_head": "e02d41bceab57756373b2dc3bcf78635b856ec81", + "pull_request": null, + "reviewed_head": null, "bundle_manifest_path": "src/policyengine/data/bundle/manifest.json", "policyengine_version": null, "bundle_manifest_sha256": null, - "gate": "draft stacked on #704; attribution 2026-10-24T22:00Z to 2026-10-25T10:00Z, then review and release" + "gate": "d2011 = A: the 2.151.0 national (microcosm#1396, a6299504) and its local-area cut certified together; fallback to #715's a133f37a pair with policyengine.py #699's area routing if not certified by 2026-10-27T18:00Z" } } diff --git a/docs/CALIBRATION_ANCHOR.md b/docs/CALIBRATION_ANCHOR.md index e7e63b27..fb86e45e 100644 --- a/docs/CALIBRATION_ANCHOR.md +++ b/docs/CALIBRATION_ANCHOR.md @@ -1,6 +1,6 @@ # Calibration anchor and certified-input bridge -> **Integration (part D).** The anchor is now part of each dataset's registry entry (`datasets.ageing_anchor`: 2025 for Enhanced FRS 1.56.16 and 1.57.4, read from `frs_release.py` at each tag; Microcosm has no published calibration year, so its anchor is 2024, the first year the ONS projection covers). *Superseded for Microcosm: every registered cut's targets are dated 2025, so each anchors at 2025 (5c6b3f68 since 95732e2). Builds from microcosm#1095 on, such as a133f37a, also write the solved weights at the data year divided by the model's own projection, so their 2025 weights are exactly the solved ones; 5c6b3f68's are the solved ones times 1.0072 ([METHOD.md](METHOD.md)).* The rake keeps the dataset's own weights in the anchor year and every year before it (`demography.annual_weights`). The level difference described below, runtime 2025 weights = calibrated weights × 1.0072 × 1.027 / 1.039 = 0.9956, is reported upstream as policyengine-uk-data#538. Re-checked on policyengine-uk 2.120.0: every 2025 weight is its 2024 weight × 1.0072. +> **Integration (part D).** The anchor is now part of each dataset's registry entry (`datasets.ageing_anchor`: 2025 for Enhanced FRS 1.56.16 and 1.57.4, read from `frs_release.py` at each tag; Microcosm has no published calibration year, so its anchor is 2024, the first year the ONS projection covers). *Superseded for Microcosm: every registered cut's targets are dated 2025, so each anchors at 2025 (5c6b3f68 since 95732e2). Builds from microcosm#1095 on (a133f37a, and d2011's a6299504 at microcosm#1396) also write the solved weights at the data year divided by the model's own projection, so their 2025 weights are exactly the solved ones; 5c6b3f68's are the solved ones times 1.0072 ([METHOD.md](METHOD.md)).* The rake keeps the dataset's own weights in the anchor year and every year before it (`demography.annual_weights`). The level difference described below, runtime 2025 weights = calibrated weights × 1.0072 × 1.027 / 1.039 = 0.9956, is reported upstream as policyengine-uk-data#538. Re-checked on policyengine-uk 2.120.0: every 2025 weight is its 2024 weight × 1.0072. The pilot uses **2025**, the calibration year configured in the exact source release for its certified Enhanced FRS. It anchors to the runtime's native 2025 weights. Those weights do not restore the builder's original calibrated weights, so preserved Housing Benefit and Pension Credit calibration remains an acceptance gate. diff --git a/docs/METHOD.md b/docs/METHOD.md index d2f1e8d4..f442e196 100644 --- a/docs/METHOD.md +++ b/docs/METHOD.md @@ -2,7 +2,7 @@ The committed `data/results.json` is the **certified pre-Budget release**, provisional and model-conditional under uncertainty ruling (c). Every fiscal figure is a full PolicyEngine UK run on policyengine.py **6.2.6**, UK **2.125.1**, core **3.33.0** and Enhanced FRS **1.58.0**, with static ageing below. That release, publicized in Vahid's post as provisional, uses Enhanced FRS as primary and the earlier Microcosm artifact as its uncertified paired sensitivity. The original uncertified model-v2 pilot (UK 2.120.0 / EFRS 1.56.16) is retained separately in `data/pilot`; model-v2 is merged. The completed historical bridge and rehearsal are bound by [J-completed-verification](../data/pilot/J-completed-verification.json). -Max's **d1983 = B** ruling on 24 October makes the binding successor's primary the national Microcosm UK Budget pair in [policyengine.py #715](https://github.com/PolicyEngine/policyengine.py/pull/715): cut `microcosm-uk-2024-25-national-20261023T075039Z-a133f37a`, UK **2.147.2**, core **3.33.3**. Enhanced FRS **1.58.2** becomes the paired sensitivity. The carrier release is pending; the draft PR is stacked on #704, with attribution scheduled for 24 October 22:00Z–25 October 10:00Z, then review. Certification fails closed until its exact released manifest is pinned. This supersedes d1727's qualification-ledger primary. [REBUILD.md](REBUILD.md) gives the fixed-data model bridge on Enhanced FRS 1.58.0 and the cross-family data bridge to Microcosm. The pre-Budget and binding releases use different data; bridge step (b) measures that move. Historical pre-Budget provenance stays at 6.2.6 / 1.58.0. Nothing is scaled or interpolated from another run. The code is the reference. +Max's **d1983 = B** ruling on 24 October put the binding successor's primary on the certified Microcosm UK Budget pair, superseding d1727's qualification-ledger primary. His **d2011 = A** ruling on 25 October chose that pair: the 2.151.0 national and local-area pair built at [microcosm#1396](https://github.com/PolicyEngine/microcosm/pull/1396), national cut `microcosm-uk-2024-25-national-20261025T091414Z-a6299504`, UK **2.151.0**, core **3.33.3**. If it is not certified by 27 October 18:00Z, the fallback is [policyengine.py #715](https://github.com/PolicyEngine/policyengine.py/pull/715)'s `a133f37a` / UK 2.147.2 pair with [#699](https://github.com/PolicyEngine/policyengine.py/pull/699)'s area routing. Enhanced FRS **1.58.2** is the paired sensitivity. policyengine.py 6.11.1 released the pair on 27 October, certified by [policyengine.py #720](https://github.com/PolicyEngine/policyengine.py/pull/720), so the fallback was not taken; certification requires that release's exact manifest, which is pinned. [REBUILD.md](REBUILD.md) gives the fixed-data model bridge on Enhanced FRS 1.58.0 and the cross-family data bridge to Microcosm. The pre-Budget and binding releases use different data; bridge step (b) measures that move. Historical pre-Budget provenance stays at 6.2.6 / 1.58.0. Nothing is scaled or interpolated from another run. The code is the reference. ## The rules (`rules.py`) @@ -23,7 +23,7 @@ A path is calendar-year CPI and earnings growth for 2027–2039, plus the statut - the calendar growth replaces `gov.economic_assumptions.yoy_growth.obr` (RPI and CPIH move by the same amount as CPI); - the statutory inputs replace the model's own (`statutory_uprating_inputs`: September CPI and May–July AWE, each at its observation date), from which policyengine-uk builds its triple lock. Without them it would use the published figures and then calendar growth plus the OBR's forecast gap. -On UK 2.147.2, dated parameter reforms passed during simulation construction rebuild the derived series, including the triple lock. The earlier 2.125.1 dict-reform route edited already-processed parameters without that rebuild. The engine uses a Scenario before data load so projected inputs follow the path, then applies explicit flat-rate amounts computed by its own rules, preserving their precision, complete horizon and earnings-path top-up. `model_horizon.py` first extends the private pension uprating, which policyengine-uk still stops at 2034; every other derived series now reaches 2039-40 by itself (the OBR series run to 2073). A one-off check when porting found the processed parameters for the central path identical with and without the old extensions on every date to 2039; a test checks that every derived series follows a path to 2039-40. +On UK 2.147.2 and 2.151.0, dated parameter reforms passed during simulation construction rebuild the derived series, including the triple lock. The earlier 2.125.1 dict-reform route edited already-processed parameters without that rebuild. The engine uses a Scenario before data load so projected inputs follow the path, then applies explicit flat-rate amounts computed by its own rules, preserving their precision, complete horizon and earnings-path top-up. `model_horizon.py` first extends the private pension uprating, which policyengine-uk still stops at 2034; every other derived series now reaches 2039-40 by itself (the OBR series run to 2073). A one-off check when porting found the processed parameters for the central path identical with and without the old extensions on every date to 2039; a test checks that every derived series follows a path to 2039-40. **What each run checks, in every year**, failing if any misses: - CPI-uprated benefit rates follow the path's CPI the year before; @@ -37,13 +37,13 @@ On UK 2.147.2, dated parameter reforms passed during simulation construction reb - the population treatment's inputs read back from the model (ages, birthdays, head flags, weights), person and benefit-unit weights are their household's, and every raked year hits its ONS growth targets to 1e-6 relative; - the additional State Pension is the pinned one under every policy, and in the data year basic + new + additional State Pension (plus the age addition when the installed model defines it) equals the reported State Pension the run counts, for every person, to £0.01; the age addition is identical under both rules in every path/year (below). -**What does not follow the path.** Dividend, property, savings and self-employment income, rents and council tax are recorded as not following it. Rents and council tax stay at their 2030 amounts from 2031, because the survey data are extended to 2030. UK 2.147.2 lacks #2557 (first released in 2.148.2): the existing `gov.benefit_uprating_cpi` parameter has uprating metadata `gov.economic_assumptions.indices.obr.lagged_cpi`, so the benefit path check uses lagged calendar CPI. The September-CPI branch applies only to later models whose benefit-index metadata identifies that statutory projection. Statutory September inputs still determine the State Pension, the engine's additional-pension pin and its earnings override independently of this benefit-index branch. +**What does not follow the path.** Dividend, property, savings and self-employment income, rents and council tax are recorded as not following it. Rents and council tax stay at their 2030 amounts from 2031, because the survey data are extended to 2030. UK 2.151.0 includes #2557 (first released in 2.148.2): `gov.benefit_uprating_cpi` has uprating metadata `gov.economic_assumptions.indices.september_cpi_uprating`, so every benefit it uprates rises each April by the path's September CPI the year before, to 0.1 point and never cut, and the benefit path check expects exactly that. April 2027 therefore uses the path's September 2026 input. UK 2.147.2 (the fallback) and the historical 2.125.1 lack #2557: their metadata is `gov.economic_assumptions.indices.obr.lagged_cpi`, and the check uses lagged calendar CPI. Any other index fails the run. Statutory September inputs still determine the State Pension, the engine's additional-pension pin and its earnings override independently of this benefit-index branch. -**Housing Benefit allowances and Savings Credit thresholds are a projection choice.** Under d1753's default, keep the installed projection and publish the alternative as a sensitivity. UK 2.147.2 lacks both the Savings Credit `maximum` parameter and #2576's `project_pension_age_allowances` and `project_savings_credit_threshold` builders (first released in 2.151.2). Its installed HB allowances and Savings Credit thresholds therefore remain on CPI. Defensive tests also cover a model with `maximum` but without linked builders: the parameter alone never enables linked derivation. The engine overrides the guarantee to follow each path's non-negative May–July earnings, while leaving those two installed projections intact. On a later model that defines the linked builders, it also refreshes their linked amounts from its exact guarantee. The foreground sensitivity command in [REBUILD.md](REBUILD.md) compares two complete central-path triple-lock/Burnham pairs on this bundle: installed CPI and the explicitly labelled guarantee-linked alternative. No fiscal comparison has run in this preparation. +**Housing Benefit allowances and Savings Credit thresholds are a projection choice.** Under d1753's default, keep the installed projection and publish the alternative as a sensitivity. UK 2.151.0, like 2.147.2, lacks both the Savings Credit `maximum` parameter and #2576's `project_pension_age_allowances` and `project_savings_credit_threshold` builders (first released in 2.151.2). Its installed HB allowances and Savings Credit thresholds therefore remain on CPI: they name lagged calendar CPI themselves, the projection they reached through `gov.benefit_uprating_cpi` on 2.147.2 ([d2011-upstream.md](d2011-upstream.md)). Defensive tests also cover a model with `maximum` but without linked builders: the parameter alone never enables linked derivation. The engine overrides the guarantee to follow each path's non-negative May–July earnings, while leaving those two installed projections intact. On a later model that defines the linked builders, it also refreshes their linked amounts from its exact guarantee. The foreground sensitivity command in [REBUILD.md](REBUILD.md) compares two complete central-path triple-lock/Burnham pairs on this bundle: installed CPI and the explicitly labelled guarantee-linked alternative. No fiscal comparison has run in this preparation. The guarantee's earnings duty ([SSAA 1992 s150A](https://www.legislation.gov.uk/ukpga/1992/5/section/150A)) does not itself prescribe future HB allowances or Savings Credit thresholds: these are separate cash amounts in [HB Regulations Schedule 3](https://www.legislation.gov.uk/uksi/2006/214/schedule/3) and [Pension Credit Regulations reg7](https://www.legislation.gov.uk/uksi/2002/1792/regulation/7). DWP describes protected HB allowances as guarantee plus maximum Savings Credit, with that uplift removed for pension-age entrants from April 2021 ([A3/2021](https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2021/a32021-housing-benefit-person-who-have-attained-the-qualifying-age-for-state-pension-credit-amendment-regulations-2021)). Savings Credit's maximum is 60% of guarantee minus threshold ([2002 Act s3(7)](https://www.legislation.gov.uk/ukpga/2002/16/section/3)). Continuing these relationships is a documented projection convention. [HB_ALLOWANCE_REVIEW.md](HB_ALLOWANCE_REVIEW.md) preserves the historical UK 2.125.1 comparisons, whose whole-CPI headline used the old calendar-CPI benefit index. -**Take-up.** In the survey runs, Housing Benefit and council tax reduction respond only for households already receiving them: nobody newly entitled starts claiming. On policyengine-uk 2.147.2, both take-up formulas use reported claims directly in data-built simulations, including Microcosm; they ignore `claims_all_entitled_benefits` there. That flag is false for every data-built family (#1831), while hand-entered situations apply its per-family seven-benefit test. Direct take-up inputs can override the defaults. Pension-age families may make new Housing Benefit claims in law and in the household calculator, but the default data-built formulas do not add new claimants. Pension Credit take-up is the dataset's own draw (`would_claim_pc`). A pension cut can make a household eligible for Pension Credit guarantee credit, which the certified model passports only when it is received. Individual-record diagnostics are private and omitted from published outputs. The engine privately computes `concentration_top10_by_year`; selecting ten records does not prove ten nonzero contributors or adequate recoverable complements, so new publishers omit that family too. Microcosm records are constructed from survey donors, so the same linked ten-record withholding applies to both dataset families. The published file never gives a record's identifier, weight, amount or contribution. +**Take-up.** In the survey runs, Housing Benefit and council tax reduction respond only for households already receiving them: nobody newly entitled starts claiming. On policyengine-uk 2.151.0 (as on 2.147.2; the formulas are byte-identical), both take-up formulas use reported claims directly in data-built simulations, including Microcosm; they ignore `claims_all_entitled_benefits` there. That flag is false for every data-built family (#1831), while hand-entered situations apply its per-family seven-benefit test. Direct take-up inputs can override the defaults. Pension-age families may make new Housing Benefit claims in law and in the household calculator, but the default data-built formulas do not add new claimants. Pension Credit take-up is the dataset's own draw (`would_claim_pc`). A pension cut can make a household eligible for Pension Credit guarantee credit, which the certified model passports only when it is received. Individual-record diagnostics are private and omitted from published outputs. The engine privately computes `concentration_top10_by_year`; selecting ten records does not prove ten nonzero contributors or adequate recoverable complements, so new publishers omit that family too. Microcosm records are constructed from survey donors, so the same linked ten-record withholding applies to both dataset families. The published file never gives a record's identifier, weight, amount or contribution. **Council tax reduction for Pension Credit recipients.** Since policyengine-uk 2.104.2 the pensioner schemes disregard the whole income of a guarantee credit recipient (SI 2012/2885, Schedule 1, paragraph 13, and the Welsh and Scottish equivalents), and use the Pension Credit assessment for a savings-credit-only award, as the law requires. A guarantee credit recipient's council tax reduction no longer moves with the State Pension. A test checks that for the example pensioner on the guarantee under both rules, council tax reduction and Housing Benefit do not move and the change in income is exactly the change in savings credit, which pays 60% of income above its threshold (and which policyengine-uk 2.118.0 and later pay the example once the basic State Pension passes the threshold, where 2.90.2 paid none). @@ -69,7 +69,7 @@ The guarantee's earnings duty ([SSAA 1992 s150A](https://www.legislation.gov.uk/ - **State Pension age.** The model's own: policyengine-uk sets it from each person's date of birth by the Pensions Act 1995 timetable, including the rise from 66 to 67 for people born from 6 April 1960 (#1899), and the engine reads it through `is_SP_age` and `state_pension_age`. Ages are held, so a record's date of birth moves a year later each year: 66-year-olds are partly over State Pension age in 2026-27 and 2027-28 and below it from 2028-29. Each run records the youngest age with anyone over it and the youngest from which everyone is (`fixed_inputs.state_pension_age`). - **Pension type**, by the treatment's rule (above). Each run reads the types back from the model in every year, fails if anyone's differs, and records the counts (`held_pension_type_records`, `held_pension_type_people`). - **The State Pension accounting, one rule under every treatment.** The reported State Pension counts only for people over State Pension age in the data year: none is payable before it (Pensions Act 2014 s.2(1)(a) and s.4(1)(a); SSCBA 1992 s.44(1); a survivor's inherited pension is their own from it, Pensions Act 2014 s.7(1)(a) and s.9(1)(a)). In the Enhanced FRS 40 records report State Pension below it, most of them within a year of it; two-fifths of them also report contributory ESA, which nobody over pensionable age can get, and two-thirds report earnings. They are reporting errors, and the model already paid them none; the data year's `state_pension_reported` is pinned without them (`demography.payable_reported`), and each run records how many there are and what they report (`state_pension_accounting`). In each year the counted report is split by that year's type at the type's data-year flat rate (`demography.pension_components`), as policyengine-uk's own formulas split it: the flat-rate part, which policyengine-uk scales by the flat rate, and the rest, the additional State Pension (SERPS, S2P and, on the new State Pension, protected payments, inherited amounts included), which the engine pins at its data-year amount grown by September CPI (published to April 2026, the path's after; never cut), for people over State Pension age that year. Historical policyengine-uk 2.120.0 and 2.125.1 scaled it by the flat rates' ratio (#1941). UK 2.147.2 includes #2336: upstream now uses September CPI from the data year. The engine retains its path-specific explicit input, which takes precedence over that formula; this is no longer described as a remaining correction to upstream law. Splitting the additional pension by each year's type is what keeps cohort types from paying anything twice: split by the survey year's type, a record moved from the basic to the new State Pension would be paid the band between the two flat rates inside its new State Pension and again as additional pension (part A measured £14.2bn in 2034-35 and £16.0bn in 2039-40). Every run checks, from the model, that in the data year basic + new + additional equals the counted report for every person (to £0.01) and nobody below State Pension age is paid, and that the additional pension is the pinned one under both rules in every year; tests check the same on a synthetic survey under every treatment, and that a 5% cut in the flat rates moves only the basic and new State Pension. -- **Pension Credit guarantee.** The standard minimum guarantee rises with May–July earnings (never cut), the minimum SSAA 1992 s150A requires; UK 2.147.2 still projects it by CPI because it lacks #2576. The engine's explicit earnings-linked override remains necessary and is the same under both pension rules. +- **Pension Credit guarantee.** The standard minimum guarantee rises with May–July earnings (never cut), the minimum SSAA 1992 s150A requires; UK 2.151.0 still projects it by lagged calendar CPI because it lacks #2576. The engine's explicit earnings-linked override remains necessary and is the same under both pension rules. **Age addition with a newer model.** When the installed model defines `state_pension_age_addition`, the engine takes the legislated addition out of the payable report before the flat-rate/additional split, matching `state_pension_reported_excluding_age_addition`: the report, age and BASIC type are the data year's, the legislated parameters come from the baseline when present, and the remainder is clamped at zero. At the reviewed #2340 head this is 25p a week from age 80, never uprated, using the model's annual 6 October age convention. policyengine-uk computes the separate addition for each simulated year. Only a positive data-year report below the addition needs an input override: the engine caps that year's addition at the payable report, because upstream's full £13 would otherwise exceed it; later years retain the model formula. Basic + new + additional + age addition then equals the payable report in the data year, and the age addition is the same under both rules, so its saving is zero. It appears separately as `state_pension_age_addition` in fiscal components, household calculations, breakdowns and coverage. With no such variable, including UK 2.125.1 and 2.147.2 (#2340 remains unmerged), the original three-part calculation and output schema apply. The reviewed names, pending-release audit and additional-pension input precedence are recorded in [REBUILD.md](REBUILD.md#pending-state-pension-patches). @@ -85,7 +85,7 @@ The revised [Pensions Act 2014 s.4](https://www.legislation.gov.uk/ukpga/2014/19 **Private cache.** The population (represented ages, birthdays, the weights and the State Pension age mask and types of every year) is computed once per dataset, treatment, anchor, set of years and model (`demography.eligibility_fingerprint`: the installed policyengine-uk and the files its State Pension age and type come from) and cached under `.cache/demography`, owner-only; every path job shares one entry, and the coverage and history jobs, with other years, have their own. A run reads the model's own `is_SP_age` back against the cached mask in every year and fails if they differ, so a change to the model's State Pension age cannot reuse stale types. Nothing that depends on a path is cached: the additional pension follows each path's September CPI. No record id, weight or amount leaves the cache; an ageing run returns no single-record diagnostic, and a failed path, coverage or history job keeps its error output in an owner-only file in an owner-only worker directory instead of the build log (`jobs.private_inputs`). -**Programme limits.** The new State Pension has no age addition, but the basic State Pension has one, 25p a week at 80 (SSCBA 1992 s.79 and Sch 4 Pt III para 8), which both the historical UK 2.125.1 bundle and the binding UK 2.147.2 candidate do not model ([policyengine-uk#2139](https://github.com/PolicyEngine/policyengine-uk/issues/2139)): in that bundle a reported one is in the additional pension. The version-aware handling above applies when a later bundle includes #2340. UK 2.147.2's Winter Fuel Payment and Scotland's Pension Age Winter Heating Payment (`pawhp`, in `other_spending`) use the shared winter-heating amount helper with jurisdiction-specific eligibility and amounts. The Scotland formula distinguishes qualifying-benefit couples from non-benefit households and their shared amounts; the old £100-per-household limitation does not describe this wheel. Later retirees' amounts come from today's records of the same age; deaths, migration and contribution histories are not simulated, and pensioners abroad are outside the survey. A dynamic projection is the longer-term route (PolicyEngine/microcosm-dynamics#501). +**Programme limits.** The new State Pension has no age addition, but the basic State Pension has one, 25p a week at 80 (SSCBA 1992 s.79 and Sch 4 Pt III para 8), which the historical UK 2.125.1 bundle, the binding UK 2.151.0 model and the 2.147.2 fallback do not model ([policyengine-uk#2139](https://github.com/PolicyEngine/policyengine-uk/issues/2139)): in that bundle a reported one is in the additional pension. The version-aware handling above applies when a later bundle includes #2340. UK 2.151.0's Winter Fuel Payment and Scotland's Pension Age Winter Heating Payment (`pawhp`, in `other_spending`) use the shared winter-heating amount helper, unchanged from 2.147.2, with jurisdiction-specific eligibility and amounts. The Scotland formula distinguishes qualifying-benefit couples from non-benefit households and their shared amounts; the old £100-per-household limitation does not describe this wheel. On 2.151.0 the payment's amounts rise each April by `gov.benefit_uprating_cpi`, rounded to the nearest 5p (#2575). Later retirees' amounts come from today's records of the same age; deaths, migration and contribution histories are not simulated, and pensioners abroad are outside the survey. A dynamic projection is the longer-term route (PolicyEngine/microcosm-dynamics#501). **Outputs of each run, by year:** - gross saving (basic and new State Pension spending) and net saving (change in `gov_balance`), with the components, for the UK and for Great Britain (households in England, Scotland and Wales, as DWP's figures are; neither dataset has a household of unknown country); @@ -177,9 +177,9 @@ The population here is the path runs': static ageing, ONS growth after the datas ## The certified model and the historical pilot (`datasets.py`) -For the binding rebuild, the primary registry entry is `microcosm_uk_2024_25@a133f37a`, national only. Its reviewed [#715](https://github.com/PolicyEngine/policyengine.py/pull/715) manifest head is `e02d41bceab57756373b2dc3bcf78635b856ec81`. It pins `policyengine/populace-uk-private`, revision `microcosm-uk-2024-25-national-20261023T075039Z-a133f37a`, path `microcosm_uk_2024_25.h5`, SHA-256 `64f83900ca3a1242fb017585234844ce89f743d32e41cd2b6558057777f914aa`. The model wheel is UK 2.147.2, SHA-256 `4b48ab1008b00610abc38192927ac9d4bb22037b6bbd9578926c2899e639f972`, with core 3.33.3. Its immutable [data release manifest](https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/microcosm-uk-2024-25-national-20261023T075039Z-a133f37a/releases/microcosm-uk-2024-25-national/release_manifest.json) and `src/policyengine/data/bundle/manifest.json` in the future released carrier establish the pair. The released carrier version and manifest digest remain unset, so `datasets.provenance` and the publication preflight refuse certification now. A draft head whose manifest still says 6.10.1 does not re-certify the already published 6.10.1 wheel. +For the binding rebuild, the primary registry entry is `microcosm_uk_2024_25@a6299504`. It pins `policyengine/populace-uk-private`, expected revision `microcosm-uk-2024-25-national-20261025T091414Z-a6299504`, path `microcosm_uk_2024_25.h5`, SHA-256 `b96a8cd625b645a409c063b1de5158532708a2f0d3c3897a794a8fb0bbd5064e`: the bytes of the staged attempt `uk-frs-calibration-attempt-20261025T091414Z-a6299504`, built at microcosm `67ee2ddf3`. That revision exists on the Hub with those bytes (checked on 26 October), and materialization verifies them before every use. The model wheel is UK 2.151.0, SHA-256 `5722f55aca3eb88e1a228e95e1a57e3bd70a4d530a5a000991565ebd8955ce35` (microcosm#1396's `uv.lock`; attested in `data/certification/d2011-model-wheel.json`), with core 3.33.3. The pair's released carrier also routes constituency and local-authority runs to its local-area cut, and the registry requires exactly that cut: its file, tag and SHA-256 in the bundle's `datasets` entry, its release manifest's URIs (and digest, where the bundle records one), and no `regional_certification`, since both halves are on 2.151.0. The certificate must cover core 3.33.3 by a rule stricter than policyengine.py's: under `core_binding` `certified` the named core, which must equal the bundle's core pin; under `bundle_pin` the pin, which a named core must equal. The released carrier is policyengine.py 6.11.1: its manifest SHA-256 `ab60903b…`, certifying pull request #720 and merge commit `610ebc42…`, and the local-area cut `microcosm-uk-2024-25-dense-20261026T105309Z-c9df4b86` (file `microcosm_uk_2024_25_dense.h5`, SHA-256 `cb3712f2…`, release manifest SHA-256 `30be4754…`) are pinned in `datasets.BINDING_*`, and `datasets.provenance` and the publication preflight certify only that installed release; a released-manifest certificate also needs its certifying pull request and commit. #715's `microcosm_uk_2024_25@a133f37a` / UK 2.147.2 entry stays registered as d2011's fallback. #715's candidate routes national only, but d2011's fallback adds #699's area routing to the 19 October dense cut (`ee5c2dbe`, built on 2.132.6), so the entry pins that cut and requires #699's record of the accepted mismatch; its bundle identities stay pending. -The lock installs base `policyengine==6.10.1` as the available API carrier, plus exact `policyengine-uk==2.147.2` and `policyengine-core==3.33.3`; it deliberately omits `[uk]`, because the currently released carrier's extra pins its previous UK line. The engine constructs `policyengine_uk` directly and materializes its own registry-pinned dataset rather than asking that extra to select the data. This is a preparatory environment, not released-pair certification. Once #715 ships, update only the authenticated carrier version and bundle-manifest digest, including the carrier version in the lock and ready bridge config, after checking the head has not changed. +The lock installs base `policyengine==6.11.1`, the released carrier, plus exact `policyengine-uk==2.151.0` and `policyengine-core==3.33.3`, without `[uk]`. While the release was pending it installed `policyengine==6.10.1` (`datasets.PREPARATORY_CARRIER`), whose extra pins its previous UK line; 6.11.1's extra pins the pair's model and core, and the lock still omits it. The engine constructs `policyengine_uk` directly and materializes its own registry-pinned dataset rather than asking that extra to select the data. `scripts/fill_release_identities.py` filled only the authenticated identities left unset in `datasets.py`, and the carrier version in the lock and ready bridge config, from the 6.11.1 tag; the lock was then re-resolved for that carrier, moving only `policyengine` ([REBUILD.md](REBUILD.md)). The paired sensitivity is Enhanced FRS 1.58.2: d1727's salary-sacrifice/uprating-table fixes and preserved household ids through cloning. Enhanced FRS 1.58.0 remains for bridge step (a). The pre-Budget release's `policyengine[uk]==6.2.6` / UK 2.125.1 / core 3.33.0 / Enhanced FRS 1.58.0 identity and historical receipt bytes remain as published; [J-certification](pilot/J-certification.md) retains their hashes. The [2.125.1-to-2.147.2 source audit](d1983-upstream.md) records the reviewed upstream guards. #2019, #2507–2509, #1913, #2336, #2458, #1831, #2518 and #2516 are present; #2557 and #2576 are absent, and #2340 remains unmerged. The behavioural HB passport preflight remains mandatory. #36's separate [2.153.4 audit](d1727-upstream.md) and fail-closed qualification-ledger implementation remain history; they do not authorize this primary. The remainder of this section records the pilot's version history, rather than the current bundle. @@ -218,7 +218,7 @@ Both Enhanced FRS releases load and compute on 2.118.0 and 2.120.0, and store th | `birth_year` is the calendar year of the date of birth (2.119.0); no rule reads it any more | nothing | No | | Universal Credit counts only the claimants' income, not their dependants' (UC Regs 2013 reg 22(1); 2.120.0) | Universal Credit baseline and offset | No | -Part A's version bridge rerun on 2.120.0 is retained as aggregate pilot evidence in [data/pilot/version_bridge.json](../data/pilot/version_bridge.json), with its calculation commit and model/data versions. It is an uncertified-pair pilot, not for quoting; [MODEL_V2_PILOT.md](MODEL_V2_PILOT.md) records the publication-support audit status. The pre-Budget release subsequently used core 3.33.0; the binding successor must authenticate #715's exact released UK 2.147.2 / core 3.33.3 pair ([REBUILD.md](REBUILD.md)). +Part A's version bridge rerun on 2.120.0 is retained as aggregate pilot evidence in [data/pilot/version_bridge.json](../data/pilot/version_bridge.json), with its calculation commit and model/data versions. It is an uncertified-pair pilot, not for quoting; [MODEL_V2_PILOT.md](MODEL_V2_PILOT.md) records the publication-support audit status. The pre-Budget release subsequently used core 3.33.0; the binding successor must authenticate d2011's exact released UK 2.151.0 / core 3.33.3 pair, or on fallback a133f37a's UK 2.147.2 / core 3.33.3 pair ([REBUILD.md](REBUILD.md)). The pilot-time open issues included #1927 (the Housing Benefit guarantee credit passport keyed on receipt), #1925 (benefit rates at announced amounts), #1913 (pension-age Housing Benefit allowances by cohort), #2019 (Pension Credit earnings disregards), #1941 (the additional State Pension's uprating), #2138 (Scotland's Pension Age Winter Heating Payment from winter 2025) and #2139 (the age addition). Their certified-port status is recorded in [J-upstream](pilot/J-upstream.md); #1927 is included in 2.125.1. diff --git a/docs/REBUILD.md b/docs/REBUILD.md index e8c4790f..b84fde22 100644 --- a/docs/REBUILD.md +++ b/docs/REBUILD.md @@ -6,36 +6,41 @@ The pre-Budget release is committed on **main**: policyengine.py 6.2.6 / UK 2.12 | Gate | What it holds back | Until then | |---|---|---| -| **d1983 = B: policyengine.py #715** | The exact released carrier manifest for national Microcosm cut `microcosm-uk-2024-25-national-20261023T075039Z-a133f37a`, UK 2.147.2 / core 3.33.3 | #715 remains a draft stacked on #704. Its attribution is scheduled for 24 October 22:00Z–25 October 10:00Z, then review. Preflight fails closed while carrier version and released-manifest digest are unset. | +| **d2011 = A: the 2.151.0 pair (microcosm#1396)** | The exact released carrier manifest for national Microcosm cut `microcosm-uk-2024-25-national-20261025T091414Z-a6299504` and its local-area cut, UK 2.151.0 / core 3.33.3 | Released: policyengine.py **6.11.1** (tag `b14ee554`, 27 October 01:30Z), certified by [#720](https://github.com/PolicyEngine/policyengine.py/pull/720) (merge `610ebc42` of reviewed head `99a3e48b`), before d2011's Tuesday 27 October 18:00Z deadline, so its fallback to #715's `a133f37a` / UK 2.147.2 pair was not taken. The eight `datasets.BINDING_*` identities are filled from the tag; preflight certifies only that exact release. | | **Binding inputs**: AWE on 20 October, September CPI on 21 October, OBR forecast on 28 October | Commit official source bytes and release identities before a fresh binding C2 handoff | A pre-Budget/dry-run file never authorizes the binding build. | | **d955 = (c)** | Only a passing original primary may get a model-conditional expected value, beside the scenario envelope | C2's frozen rule remains unchanged; primary failure automatically takes (a), scenarios only. No alternative form is promoted. | -## Pending #715 bundle and two-step bridge +## d2011 bundle and two-step bridge -The reviewed [#715 head](https://github.com/PolicyEngine/policyengine.py/pull/715) is `e02d41bceab57756373b2dc3bcf78635b856ec81`. Check that head again before completing the pins; if its cut/model changes, re-read the new manifest and audit, and if it closes, stop. Its exact identities are: +Max ruled **d2011 = A** on 25 October: the binding primary is the certified 2.151.0 national and local-area pair built at [microcosm#1396](https://github.com/PolicyEngine/microcosm/pull/1396) (`67ee2ddf3`). d1983 = B's reason (Microcosm, clean data) holds for it. Its identities, known before the release: | Identity | Pin | |---|---| -| UK model/core | `policyengine-uk==2.147.2`, `policyengine-core==3.33.3` | -| UK wheel SHA-256 | `4b48ab1008b00610abc38192927ac9d4bb22037b6bbd9578926c2899e639f972` | -| Dataset registry name | `microcosm_uk_2024_25@a133f37a`, national only | -| HF repository / revision / path | `policyengine/populace-uk-private` / `microcosm-uk-2024-25-national-20261023T075039Z-a133f37a` / `microcosm_uk_2024_25.h5` | -| Dataset SHA-256 | `64f83900ca3a1242fb017585234844ce89f743d32e41cd2b6558057777f914aa` | -| Data release manifest | `https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/microcosm-uk-2024-25-national-20261023T075039Z-a133f37a/releases/microcosm-uk-2024-25-national/release_manifest.json` | -| Source manifest URI | `hf://dataset/policyengine/populace-uk-private@microcosm-uk-2024-25-national-20261023T075039Z-a133f37a/releases/microcosm-uk-2024-25-national/release_manifest.json` | -| Released carrier manifest | Future `policyengine` release's `src/policyengine/data/bundle/manifest.json`; carrier version and digest intentionally unset | - -The draft manifest still labels itself 6.10.1, but the already released 6.10.1 wheel carries the previous UK cut and cannot certify this one. The preparatory lock uses the base carrier without `[uk]`, plus explicit UK 2.147.2/core 3.33.3. The engine constructs `policyengine_uk` directly and loads its own verified artifact; the released carrier's `[uk]` extra would impose the previous model. Publication preflight remains closed until the exact #715 release is authenticated. d1727's Enhanced FRS qualification-ledger route is superseded as the primary; its fail-closed code, [status receipt](../data/certification/d1727-qualification-status.json) and [2.153.4 audit](d1727-upstream.md) stay as history. - -1. After #715 is reviewed and released, set `datasets.BINDING_BUNDLE_VERSION` and `datasets.BINDING_BUNDLE_MANIFEST_SHA256` from the future released carrier, and the carrier version in both dependency files. Do not change the model/core/data pins merely to fit a carrier. Confirm that the released manifest exactly matches every identity above. -2. Keep the current [2.125.1-to-2.147.2 source audit](d1983-upstream.md), `check_upstream()` and the parameter/small-household checks passing. UK 2.147.2 includes #2019, #2507–2509, #1913, #2336, #2458, #1831, #2518 and #2516. It lacks #2557 (September-CPI benefit uprating, first 2.148.2) and #2576 (earnings-linked guarantee/derived HB and Savings Credit, first 2.151.2); #2340 remains unmerged. Additional pension still takes the engine's path-specific explicit CPI pin. Age-addition handling takes the absent-variable branch. - - **If d2011 certifies the 2.151.0 pair instead** (María's national cut `a6299504`, microcosm#1396), its [2.147.2-to-2.151.0 source audit](d2011-upstream.md) applies, and `check_upstream()` already accepts 2.151.0 exactly. That wheel adds #2557, so benefit uprating follows September CPI and `path_following` takes its September branch. It still lacks #2576 and #2340, so d1753's installed projections and the absent-variable age-addition branch are unchanged. The model pin, `datasets.py`, the bridge configuration's endpoints and the release wording then move to the certified release in one commit, and steps 4–5 run on it: (a) UK 2.125.1 → 2.151.0, then (b) Enhanced FRS 1.58.0 → Microcosm a6299504. +| UK model/core | `policyengine-uk==2.151.0`, `policyengine-core==3.33.3` (microcosm#1396's `uv.lock`) | +| UK wheel SHA-256 | `5722f55aca3eb88e1a228e95e1a57e3bd70a4d530a5a000991565ebd8955ce35` (`data/certification/d2011-model-wheel.json`) | +| Dataset registry name | `microcosm_uk_2024_25@a6299504` | +| HF repository / revision / path | `policyengine/populace-uk-private` / `microcosm-uk-2024-25-national-20261025T091414Z-a6299504` / `microcosm_uk_2024_25.h5` | +| Dataset SHA-256 | `b96a8cd625b645a409c063b1de5158532708a2f0d3c3897a794a8fb0bbd5064e`, the staged attempt `uk-frs-calibration-attempt-20261025T091414Z-a6299504` | +| Data release manifest | `releases/microcosm-uk-2024-25-national/release_manifest.json` at that revision (`datasets.MICROCOSM_RELEASE_MANIFEST`) | +| Region routing | national to the national file; constituency and local authority to the local-area cut's file, with the bundle's `regional_*` fields naming that cut's release manifest and no `regional_certification` (both halves are on 2.151.0) | +| Filled from the 6.11.1 release (`datasets.BINDING_*`) | carrier `6.11.1`, manifest SHA-256 `ab60903b390e7a765ea6558ec5e2d8fa5aede06b6f735fbf4e4c96109ffd1617`, certifying pull request #720 and merge commit `610ebc42586b7358c41afe4fabec7c67adf17205`, and the 2.151.0 local-area cut: file `microcosm_uk_2024_25_dense.h5`, tag `microcosm-uk-2024-25-dense-20261026T105309Z-c9df4b86`, SHA-256 `cb3712f25b76109ad27b7b0cdf5dee78c55d3b7f55ce7bf207ef1a191da2ae9a` and release manifest SHA-256 `30be47545efd2aa320e30f8d04240cf345aec105fa1d788334770bc4e17c583e` | +| Core coverage | the certificate's core by policyengine.py's rule (`datasets.certified_core`), made stricter: the named `certified_for_core_version` under `core_binding` `certified`, which must equal the bundle's core pin; else the pin (`bundle_pin`, 6.11.0's default), which a named core must equal | + +The revision exists on the Hub with the pinned file (LFS SHA-256 and its 342,348,841 bytes checked on 26 October); materialization still verifies the bytes before every use, and the release must name the same artifact. The lock uses the released base carrier `policyengine==6.11.1` without `[uk]`, plus explicit UK 2.151.0/core 3.33.3. The engine constructs `policyengine_uk` directly and loads its own verified artifact. Publication preflight certifies only the exact authenticated release. d1727's Enhanced FRS qualification-ledger route is superseded as the primary; its fail-closed code, [status receipt](../data/certification/d1727-qualification-status.json) and [2.153.4 audit](d1727-upstream.md) stay as history. + +**Fallback (not taken).** The pair shipped in 6.11.1 before d2011's deadline, so this fallback was not used; it stays documented, and its identities stay pending. d2011's fallback was #715's `microcosm_uk_2024_25@a133f37a` / UK 2.147.2 pair served with [policyengine.py #699](https://github.com/PolicyEngine/policyengine.py/pull/699)'s area routing: a133f37a's national file, with constituency and local-authority runs routed to the 19 October dense local-area cut `microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe` (built on 2.132.6) on an accepted model mismatch. #715's reviewed candidate head `e02d41bceab57756373b2dc3bcf78635b856ec81` routes national only, so it does not certify the fallback by itself. Its local-area cut is pinned now (`datasets.FALLBACK_LOCAL_AREA`: file `microcosm_uk_2024_25_dense.h5`, SHA-256 `8e10e130…`, release manifest SHA-256 `b9dc7cae…`), and the release must carry #699's `regional_certification` for exactly that mismatch (2.147.2 certified, 2.132.6 built, area receipts uncertified) and `regional_source_manifest_sha256`. Its bundle identities stay pending in `datasets.FALLBACK_*`. If d2011 falls back, fill them with `HUGGING_FACE_TOKEN=… .venv/bin/python scripts/fill_release_identities.py --target fallback --pr --release --apply`. Then, in one reviewed commit, move these to the fallback: `config.PRIMARY_DATASET` (the preflight reads the bundle of whichever of `datasets.D2011_PAIRS` it names, here `datasets.BUNDLE_715`); the model pin to `policyengine-uk==2.147.2` and the carrier pin in `pyproject.toml` and `requirements-lock.txt`, with the lock re-resolved as in step 1; CI's install comment; the bridge endpoints and dataset in `data/binding-bridge.pending.json`, and its ready copy; and the d2011 wording in METHOD.md and this runbook. Its [2.125.1-to-2.147.2 audit](d1983-upstream.md) applies. + +1. Done for 6.11.1 on 27 October (`--pr 720 --release 6.11.1`): all 32 checks passed at the tag, and all 37 on the installed wheel. After the pair is released, run `HUGGING_FACE_TOKEN=… .venv/bin/python scripts/fill_release_identities.py --pr --release `. It reads the UK bundle manifest at the release tag (the release workflow's version commit rewrites it after the certifying merge, so the merge commit's bytes are not the release's) and compares every identity above with it, the revision included. It reads the local-area cut's own release manifest at the cut's tag (or `--regional-manifest FILE`): the cut's build must name that tag, have run UK 2.151.0 on core 3.33.3, and publish the file the bundle routes to, with the bundle's `datasets` entry, regional URIs and any `regional_source_manifest_sha256` matching; a `regional_certification` block refuses. It requires the certifying pull request to be merged, its merge commit to be in the tag, and the tag's manifest to be, byte for byte, what the release workflow writes from the manifest at that commit: it rewrites the carrier version fields, each country's bundle id, the install requirements and the citation, drops `source`, and changes nothing else. It prints the eight `datasets.BINDING_*` values: carrier version, manifest SHA-256, certifying pull request and merge commit, and the local-area cut's file, tag, SHA-256 and release manifest SHA-256. Any difference refuses. With `--apply` (only with `--release`) it checks every file it writes before writing any, then writes those, the carrier version in both dependency files and the ready `data/binding-bridge.json` of step 4. Do not change the model/core/data pins merely to fit a carrier. + Then re-resolve the lock for the new carrier, keeping every other pin as a preference: save the four comment lines at the top of `requirements-lock.txt`, run `uv pip compile pyproject.toml --extra uk --extra dev --python-version 3.13 --no-header --no-annotate --upgrade-package policyengine -o requirements-lock.txt`, and put the comment lines back. At the preparatory carrier this reproduces all 86 pins exactly. Name any pin other than `policyengine` that moves in the commit; `test_the_lock_satisfies_the_installed_carrier_s_own_requirements` checks the installed carrier's requirements against the lock. After installing the lock, rerun the script without `--apply`, with `--manifest` on the installed wheel's `policyengine/data/bundle/manifest.json`: it then also checks each filled identity, so the installed bytes must hash to the pinned digest, as the preflight in step 3 requires. With the release installed, `tests/test_datasets.py` and `tests/test_budget_bundle.py` require provenance to certify the filled pair. +2. Keep the [2.147.2-to-2.151.0 source audit](d2011-upstream.md) (on top of the [2.125.1-to-2.147.2 audit](d1983-upstream.md)), `check_upstream()` and the parameter/small-household checks passing. UK 2.151.0 adds #2557: benefit uprating follows the previous September's CPI and `path_following` takes its September branch. It lacks #2576 (earnings-linked guarantee/derived HB and Savings Credit, first 2.151.2), and #2340 remains unmerged. Additional pension still takes the engine's path-specific explicit CPI pin. Age-addition handling takes the absent-variable branch. 3. Run certification and the behavioural HB receipt-passport preflight below before survey jobs. Synthetic checks never certify the fiscal result. -4. Copy `data/binding-bridge.pending.json` to `data/binding-bridge.json`. Fill the released carrier version in the three new endpoint package maps and `pending_release.policyengine_version`, fill `pending_release.bundle_manifest_sha256`, set status `ready`, and commit it. Keep historical baseline pins/SHA, UK/core pins and both artifact hashes fixed. Pending `--validate-only` honestly refuses #715 without launching jobs. -5. Run **(a), model step**: 6.2.6 / UK 2.125.1 / core 3.33.0 → UK 2.147.2 / core 3.33.3, holding **Enhanced FRS 1.58.0** fixed. The new intermediate pair is uncertified. Run **(b), data step**: **Enhanced FRS 1.58.0 → Microcosm a133f37a**, holding the same installed packages and engine sources fixed. Only that final endpoint must have released certification. Each endpoint runs the complete central/coverage/frozen forty-path design. Data/from must reuse exactly model/to's receipt; no dataset-family equality is assumed. +4. `--apply` in step 1 writes this; check it by hand. It copies `data/binding-bridge.pending.json` to `data/binding-bridge.json`, fills the released carrier version in the three new endpoint package maps and `pending_release.policyengine_version`, fills `pending_release.pull_request`, `reviewed_head` and `bundle_manifest_sha256`, and sets status `ready`. Commit it. Keep historical baseline pins/SHA, UK/core pins and both artifact hashes fixed. Pending `--validate-only` honestly refuses without launching jobs. +5. Run **(a), model step**: 6.2.6 / UK 2.125.1 / core 3.33.0 → UK 2.151.0 / core 3.33.3, holding **Enhanced FRS 1.58.0** fixed. The new intermediate pair is uncertified. Run **(b), data step**: **Enhanced FRS 1.58.0 → Microcosm a6299504**, holding the same installed packages and engine sources fixed. Only that final endpoint must have released certification. Each endpoint runs the complete central/coverage/frozen forty-path design. Data/from must reuse exactly model/to's receipt; no dataset-family equality is assumed. 6. Review fresh accounting and GB/DWP coverage before feeding a regenerated binding C2 handoff to the full fiscal build. The primary/sensitivity config change alters its input hash: regenerate it through C2, and preserve every historical handoff/receipt byte. -**d1753 on this bundle.** UK 2.147.2 has neither Savings Credit `maximum` nor either linked projection builder. Tests also cover a `maximum`-present/builders-absent system defensively. Keep its installed **CPI** HB allowances and Savings Credit thresholds as headline. Their `gov.benefit_uprating_cpi` parameter names `gov.economic_assumptions.indices.obr.lagged_cpi` in its uprating metadata, so it follows lagged calendar CPI; #2557's September-CPI construction is absent. The engine still applies the statutory earnings-linked guarantee. Gate linked derivation on the actual builders, not merely on `maximum`. The separate foreground comparison below runs the installed-CPI and explicitly guarantee-linked conventions as complete central triple-lock/Burnham pairs; it does not change the default. +**Microcosm's anchor year.** Both Microcosm cuts anchor static ageing at 2025, their calibration year (#40). Each build solves its household weights at 2025 and writes them at the 2024 data year divided by policyengine-uk's own projection factor (1.0072), so the model's 2025 weights are the solved ones (`build_record.json`'s export readback `weight_year`; microcosm's `weight_year.py`, microcosm#796 A19). a133f37a's entry had said it declared no calibration year. + +**d1753 on this bundle.** UK 2.151.0 has neither Savings Credit `maximum` nor either linked projection builder. Tests also cover a `maximum`-present/builders-absent system defensively. Keep its installed **CPI** HB allowances and Savings Credit thresholds as headline. Since #2557 moved `gov.benefit_uprating_cpi` to September CPI, those amounts name `gov.economic_assumptions.indices.obr.lagged_cpi` themselves, so they keep the lagged calendar-CPI projection they had on 2.147.2. The engine still applies the statutory earnings-linked guarantee. Gate linked derivation on the actual builders, not merely on `maximum`. The separate foreground comparison below runs the installed-CPI and explicitly guarantee-linked conventions as complete central triple-lock/Burnham pairs; it does not change the default. ```sh # Main session foreground only, after authenticating and installing the released lock. @@ -53,12 +58,12 @@ export OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 NUMEXPR_NUM_TH # raise it for each Microcosm run (one worker, ~31.8 GB historical peak, plus the coordinator). # (a) Model step on Enhanced FRS 1.58.0; intermediate remains uncertified. HEAVY_CALLER=uk-triple-lock .venv/bin/python scripts/resume_after_host_refusal.py \ - --log .cache/resume/d1983-bridge-model.jsonl -- ~/reviews/us-hub/scripts/heavy_run.sh d1983-bridge-model \ + --log .cache/resume/d2011-bridge-model.jsonl -- ~/reviews/us-hub/scripts/heavy_run.sh d2011-bridge-model \ .venv/bin/python scripts/run_model_v2_j_bridge.py --config data/binding-bridge.json --step model \ --from-receipt data/pilot/J-bridge-certified.json --workers 1 --out data/pilot/binding-bridge-model.json -# (b) Cross-family data step on exactly the same UK2.147.2/core3.33.3 environment. +# (b) Cross-family data step on exactly the same UK2.151.0/core3.33.3 environment. HEAVY_CALLER=uk-triple-lock HEAVY_CAP_MB=49152 .venv/bin/python scripts/resume_after_host_refusal.py \ - --log .cache/resume/d1983-bridge-data.jsonl -- ~/reviews/us-hub/scripts/heavy_run.sh d1983-bridge-data \ + --log .cache/resume/d2011-bridge-data.jsonl -- ~/reviews/us-hub/scripts/heavy_run.sh d2011-bridge-data \ .venv/bin/python scripts/run_model_v2_j_bridge.py --config data/binding-bridge.json --step data \ --from-receipt data/pilot/binding-bridge-model.json --workers 1 --out data/pilot/binding-bridge-data.json # Send Fleet ops the data step's measured memory: per batch, timings[].memory_observed. @@ -101,7 +106,7 @@ PY_CHECK -The current State Pension guard audit is [d1983-upstream](d1983-upstream.md). #36's parameter-consistency and age-addition preparation remains documented in [d1727-upstream](d1727-upstream.md) and its retained receipts; its 2.153.4 linked-projection findings do not describe 2.147.2. +The current State Pension guard audit is [d2011-upstream](d2011-upstream.md), on top of [d1983-upstream](d1983-upstream.md). #36's parameter-consistency and age-addition preparation remains documented in [d1727-upstream](d1727-upstream.md) and its retained receipts; its 2.153.4 linked-projection findings do not describe 2.147.2 or 2.151.0. ## Inputs to update first @@ -124,20 +129,20 @@ Each is a commit before the build. The build refuses a dirty tree, and every inp - The adapter retains **(b)** as an explicit tested input for the original primary with its frozen-screen failure retained. It is not Max's selected rebuild route. A run on today's inputs is explicitly a **dry run**, not the binding score and not permission for a fiscal expected value. The adapter consumes the original-primary 160-slot design (161 unique full runs including its identical-rates check on the committed inputs). Replacement sampling can repeat indices after an input update; use the handoff's saved distinct counts. A passing binding score adds the exact frozen 40-slot paired subsample on Enhanced FRS 1.58.2; only the dataset role changes. The original-primary ±0.5-point mean paths run independently of adequacy, with the same primary indices, innovations and zero-stratum mass; their extra baseline check may save nonzero. The scenario envelope carries the central path, OBR wedge, recorded historical last-decade replay (April 2017–2026, with its own model years) and those paired mean paths under either outcome. The historical replay retains its period label and is not treated as a future-horizon simulation or included in a common future-year range. -4. **Versions and dataset**: complete the pending #715 release and two-step bridge above. The installed packages, primary artifact and released certification must match exactly. Run the final coverage and accounting checks before the binding run. The behavioural Housing Benefit Guarantee Credit passport must still pass: the non-claimant faces the ordinary means test, the claimant receives maximum HB, and reversed receipt flags exercise all three branches. Every publication entry point runs this guard before fiscal jobs. +4. **Versions and dataset**: with d2011's release filled (6.11.1), complete the two-step bridge above. The installed packages, primary artifact and released certification must match exactly. Run the final coverage and accounting checks before the binding run. The behavioural Housing Benefit Guarantee Credit passport must still pass: the non-claimant faces the ordinary means test, the claimant receives maximum HB, and reversed receipt flags exercise all three branches. Every publication entry point runs this guard before fiscal jobs. ## Commands -Start from `main`, with the newly released #715 carrier lock and bridge changes reviewed on the rebuild branch. The preparatory lock pins its UK 2.147.2/core 3.33.3 candidate and remains uncertified until that release exists: +Start from `main`, with the newly released d2011 carrier lock and bridge changes reviewed on the rebuild branch. The lock pins the released carrier, policyengine.py 6.11.1, with UK 2.151.0/core 3.33.3: Set `HEAVY_CALLER=uk-triple-lock` before every command run through `heavy_run.sh`, `lockrun.sh` or `heavy_queue.zsh`. The caller identity gives this repository access to the Fleet ops reservation [below](#part-j-pre-budget-rehearsal-and-post-budget-sequence). ```sh git fetch origin main git switch -c rebuild/budget-2026 origin/main -# Complete the pending-bundle and input-update sections above on this branch. +# Complete the bridge and input-update sections above on this branch. # Commit each step and open a draft PR into main at the first commit. -# The following install uses the newly authenticated #715 lock. +# The following install uses the newly authenticated d2011 lock. uv venv .venv --python 3.13 uv pip install --python .venv/bin/python -r requirements-lock.txt uv pip install --python .venv/bin/python --no-deps -e . @@ -151,7 +156,7 @@ python -c "import psutil; m = psutil.virtual_memory(); print(m.available / 2**30 python -m pytest -q # only the documented stale-results assertions may fail (below) # 2. After 28 October, with the binding macro inputs committed and the -# released #715 bundle installed, both bridge steps and coverage reviewed. +# released d2011 bundle installed, both bridge steps and coverage reviewed. # Check #1927's receipt passport before scoring; real builds also run this guard. python -c 'from triple_lock.pipeline import housing_benefit_passport_preflight; housing_benefit_passport_preflight()' python -m triple_lock.ts_uncertainty --screen c2 --binding --output out/uncertainty-c2 @@ -201,7 +206,7 @@ Step 3 reads its paired draws from the file it is pointed at (`data/results.json ## Interim pre-Budget release (before the binding rebuild) -This section describes the historical certified pre-Budget release, reproduced only at its original source and lock. Its 6.2.6 / 1.58.0 Enhanced FRS primary and earlier Microcosm sensitivity remain as published under Max's recorded ruling (c). The current binding source selects different datasets and must use the #715 sequence above. Its expected value is **provisional and model-conditional**: the original primary passes the registered C2 dry run on current inputs (4 of 6 terminal years, 80% annual coverage), with the full paired Microcosm cross-check. The binding test follows the Budget inputs; failure removes the expected value. A scenarios-only interim release may use (a). The obsolete public pre-Budget (b) route is refused. +This section describes the historical certified pre-Budget release, reproduced only at its original source and lock. Its 6.2.6 / 1.58.0 Enhanced FRS primary and earlier Microcosm sensitivity remain as published under Max's recorded ruling (c). The current binding source selects different datasets and must use the d2011 sequence above. Its expected value is **provisional and model-conditional**: the original primary passes the registered C2 dry run on current inputs (4 of 6 terminal years, 80% annual coverage), with the full paired Microcosm cross-check. The binding test follows the Budget inputs; failure removes the expected value. A scenarios-only interim release may use (a). The obsolete public pre-Budget (b) route is refused. ```sh # From a clean checkout of the reviewed head, venv as above, Enhanced FRS 1.58.0 and Microcosm in .cache/datasets. @@ -243,9 +248,9 @@ The dataset roles have changed. Counts below use the committed original-primary For updated inputs let `U` be original-primary `unique_full_runs` (including the extra check), and `V` the distinct indices in the frozen 40-slot sensitivity. The binding build has `3U + 11` **Microcosm** jobs under either C2 outcome; Enhanced FRS adds `V + 1` after a pass, or one after failure. Validation adds `5(1 + V) + 6` Microcosm jobs after pass, or 11 after failure. Standalone mean-path scenarios have `3U` Microcosm jobs. Cache hits and replacement-draw duplicates reduce process launches. -**Runtime at one Microcosm worker.** The only cold Microcosm path timings are the 12 October rehearsal's two jobs (556 s and 672 s, mean 614 s), on an earlier cut and model. At that rate, one worker runs the build's 494 Microcosm jobs in about **84 hours**, and the validation's 211 after a pass in about **36 hours** more. That is an estimate from two historical jobs, not a measurement on a133f37a. The binding data step runs 42 Microcosm jobs at one worker and records each batch's time and memory: use them to re-estimate, and to decide with Fleet ops whether a second Microcosm worker fits (two workers need 80 GiB available at a batch start, and each slot refill 40 GiB beyond what the other running job may still allocate of its 40 GiB). +**Runtime at one Microcosm worker.** The only cold Microcosm path timings are the 12 October rehearsal's two jobs (556 s and 672 s, mean 614 s), on an earlier cut and model. At that rate, one worker runs the build's 494 Microcosm jobs in about **84 hours**, and the validation's 211 after a pass in about **36 hours** more. That is an estimate from two historical jobs, not a measurement on a6299504. The binding data step runs 42 Microcosm jobs at one worker and records each batch's time and memory: use them to re-estimate, and to decide with Fleet ops whether a second Microcosm worker fits (two workers need 80 GiB available at a batch start, and each slot refill 40 GiB beyond what the other running job may still allocate of its 40 GiB). -**Capacity estimate for Fleet ops, derived without survey jobs.** The old reservation assumed 3 Enhanced FRS plus 2 Microcosm workers. Keeping role caps of 3 primary plus 2 sensitivity would now mean **3 Microcosm plus 2 Enhanced FRS** workers. Across concurrent work, filling all five reserved slots at the historical per-worker peaks (31.8 GB and 6 GB) gives **107.4 GB**, versus the old **81.6 GB**: **25.8 GB / 31.6% more**. These are reservation-wide sums. The frozen `expected_value.build` schedules primary and sensitivity phases sequentially: its three-primary/two-sensitivity worker estimate is `max(3 × 31.8, 2 × 6)` = **95.4 GB**, excluding coordinator/other-process memory. At the documented one-primary/one-sensitivity caps, that phase maximum is **31.8 GB**. These are historical-peak extrapolations, not measurements of a133f37a/2.147.2; the fresh-cut peak and net-support-array cost are unmeasured. Code admission requires **120 GiB available** for three concurrent cold Microcosm jobs (40 GiB each), compared with 80 GiB for two. Start the main session at **`--workers 1 --sensitivity-workers 1`**, with a 40 GiB admission floor, and have Fleet ops review capacity before increasing it. The bridge likewise caps Microcosm endpoints at two, uses Microcosm slots and scales its before-batch floor to 40 GiB per concurrent Microcosm job; each slot keeps 40 GiB for the one job it admits beyond what the batch's other admitted Microcosm jobs may still allocate, and its monitor keeps 8 GiB while the batch runs ([Host admission](#host-admission-memory-floors-and-the-load-limit)). +**Capacity estimate for Fleet ops, derived without survey jobs.** The old reservation assumed 3 Enhanced FRS plus 2 Microcosm workers. Keeping role caps of 3 primary plus 2 sensitivity would now mean **3 Microcosm plus 2 Enhanced FRS** workers. Across concurrent work, filling all five reserved slots at the historical per-worker peaks (31.8 GB and 6 GB) gives **107.4 GB**, versus the old **81.6 GB**: **25.8 GB / 31.6% more**. These are reservation-wide sums. The frozen `expected_value.build` schedules primary and sensitivity phases sequentially: its three-primary/two-sensitivity worker estimate is `max(3 × 31.8, 2 × 6)` = **95.4 GB**, excluding coordinator/other-process memory. At the documented one-primary/one-sensitivity caps, that phase maximum is **31.8 GB**. These are historical-peak extrapolations, not measurements of a6299504/2.151.0; the fresh-cut peak and net-support-array cost are unmeasured. Code admission requires **120 GiB available** for three concurrent cold Microcosm jobs (40 GiB each), compared with 80 GiB for two. Start the main session at **`--workers 1 --sensitivity-workers 1`**, with a 40 GiB admission floor, and have Fleet ops review capacity before increasing it. The bridge likewise caps Microcosm endpoints at two, uses Microcosm slots and scales its before-batch floor to 40 GiB per concurrent Microcosm job; each slot keeps 40 GiB for the one job it admits beyond what the batch's other admitted Microcosm jobs may still allocate, and its monitor keeps 8 GiB while the batch runs ([Host admission](#host-admission-memory-floors-and-the-load-limit)). The historical rehearsal timings at the end of this runbook used the opposite data roles and an earlier cut; they do not forecast the binding runtime. No current survey or Microcosm simulation was run for this preparation. @@ -307,7 +312,7 @@ The bridge also measures memory while each batch runs, in `timings[].memory_obse - `peak_job_footprint_gib`, the largest kernel lifetime footprint peak (`phys_footprint_peak`) of any job process, as of the last sample; - `peak_finished_job_rss_gib`, the kernel's high-water resident memory of the largest job process finished so far in the run (`getrusage`). -Resident memory excludes compressed and swapped pages, so under memory pressure it understates a job's footprint; the footprint fields count them (they are null off macOS). A slot credits each running job with its resident memory. For compressed and swapped pages that errs safe: a smaller reading leaves a larger reservation. For pages a job shares with other processes it does not: they count as held, though copying them later adds physical memory that resident size does not show. On 25 October a process importing the locked model and its libraries measured 0.252 GiB resident against a 0.225 GiB footprint; the binding data step records both measures for a loaded worker. The reservations have three further limits. On macOS `psutil` 6.1.1 reports available memory as inactive plus free pages, so a running job's own inactive pages can count as available while its resident memory is also credited as held; with one Microcosm worker, as the reservation sets, no other job in the batch is credited at all. Reservations are per process: a build and a bridge running at once could each admit a job on the same headroom, so the runbook never overlaps them. The historical rehearsal runner keeps its original admission (40 GiB in each slot, no reservations), because its receipts record its source; reproduce its Microcosm stages with one worker. The binding data step at one worker is the first measurement of a Microcosm worker on a133f37a / 2.147.2. Read its peak before raising `--workers` for the build, and re-set `HEAVY_CAP_MB` to at least 1.3 × the measured tree footprint (Fleet ops' rule: a ceiling trip closes heavy slot 2 for everyone for an hour). The limit is operational. It is in no job's cache key, recipe hash or execution snapshot, so changing it reruns nothing. +Resident memory excludes compressed and swapped pages, so under memory pressure it understates a job's footprint; the footprint fields count them (they are null off macOS). A slot credits each running job with its resident memory. For compressed and swapped pages that errs safe: a smaller reading leaves a larger reservation. For pages a job shares with other processes it does not: they count as held, though copying them later adds physical memory that resident size does not show. On 25 October a process importing the locked model and its libraries measured 0.252 GiB resident against a 0.225 GiB footprint; the binding data step records both measures for a loaded worker. The reservations have three further limits. On macOS `psutil` 6.1.1 reports available memory as inactive plus free pages, so a running job's own inactive pages can count as available while its resident memory is also credited as held; with one Microcosm worker, as the reservation sets, no other job in the batch is credited at all. Reservations are per process: a build and a bridge running at once could each admit a job on the same headroom, so the runbook never overlaps them. The historical rehearsal runner keeps its original admission (40 GiB in each slot, no reservations), because its receipts record its source; reproduce its Microcosm stages with one worker. The binding data step at one worker is the first measurement of a Microcosm worker on a6299504 / 2.151.0. Read its peak before raising `--workers` for the build, and re-set `HEAVY_CAP_MB` to at least 1.3 × the measured tree footprint (Fleet ops' rule: a ceiling trip closes heavy slot 2 for everyone for an hour). The limit is operational. It is in no job's cache key, recipe hash or execution snapshot, so changing it reruns nothing. The full build (`triple-lock-build`), standalone scenarios and `scripts/validate_ageing.py` use the default scheduler admission described in the binding-run guards above. @@ -375,7 +380,7 @@ No history fetch is needed in the checkout workflow. ## Part J: pre-Budget rehearsal and post-Budget sequence -The historical rehearsal used the pre-Budget 6.2.6 / 1.58.0 certified bundle. Reproducing that historical run uses that exact lock; the pending binding run uses d1983/#715 instead. The commands in this subsection reproduce the historical rehearsal at its original source and lock; the current foreground commands above control the binding rebuild. Its numbers are not for quoting. It never writes `data/results.json`, its dashboard copy or scenario publication files. The C2 dry handoff remains `run_kind: dry_run` with `expected_value_authorized: false`; the rehearsal adapter retains the frozen ruling (c) checks and original-primary design. A passing diagnostic screen does not authorize a binding build. +The historical rehearsal used the pre-Budget 6.2.6 / 1.58.0 certified bundle. Reproducing that historical run uses that exact lock; the binding run uses d2011's pair instead. The commands in this subsection reproduce the historical rehearsal at its original source and lock; the current foreground commands above control the binding rebuild. Its numbers are not for quoting. It never writes `data/results.json`, its dashboard copy or scenario publication files. The C2 dry handoff remains `run_kind: dry_run` with `expected_value_authorized: false`; the rehearsal adapter retains the frozen ruling (c) checks and original-primary design. A passing diagnostic screen does not authorize a binding build. ```sh export OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 NUMEXPR_NUM_THREADS=1 VECLIB_MAXIMUM_THREADS=1 @@ -406,7 +411,7 @@ The exact post-Budget order is: ``` A `ready` mark brings the start forward only; it cannot extend the window past 4 November. -1. Branch from `origin/main` and open a new draft PR into `main` at the first commit. Complete the pending #715 release, upstream re-hash, passport preflight, model bridge, data bridge and coverage review above. Commit AWE (20 October), September CPI (21 October), the OBR forecast (28 October) and newly complete backtest origins with official source bytes and release identities. Recheck the certified lock and guards after those changes. +1. Branch from `origin/main` and open a new draft PR into `main` at the first commit. With d2011's release filled (6.11.1), complete the upstream re-hash, passport preflight, model bridge, data bridge and coverage review above. Commit AWE (20 October), September CPI (21 October), the OBR forecast (28 October) and newly complete backtest origins with official source bytes and release identities. Recheck the certified lock and guards after those changes. 2. Run the **binding C2 re-score**, using `python -m triple_lock.ts_uncertainty --screen c2 --binding --output out/uncertainty-c2`. It is not a separate launchd label. It is the `uk-triple-lock-c2` step inside the loaded launchd job `com.maxghenis.triple-lock-2027`, which runs daily at 08:35. From **29 October 2026**, or the day after a moved Budget, that job dispatches the step as a Subfleet hard-tier build in this repository. It does so once its `budget-check` step has ended DONE or WAITING_ON_MAX. The step stops at the C2 verdict; it does not run the fiscal rebuild. The binding-input guard remains mandatory when the job invokes the command. Commit the resulting binding handoff and scores before model jobs. 3. Run `triple-lock-build --workers 1 --sensitivity-workers 1 --uncertainty-ruling c --uncertainty-handoff out/uncertainty-c2` on the certified bundle. Primary pass permits only its model-conditional expected value beside the scenario envelope; primary failure automatically uses (a), scenarios only. Run the paired four-way validation after a pass, or `--central-only` after a failure. Check accounting, coverage, source/input/provenance bindings, full pytest and dashboard checks. 4. Obtain independent review of the rebuilt fiscal aggregates, C2 outcome, coverage and limitations, resolve findings and repeat to approval. Keep the new rebuild PR draft until independent review approves; mark it ready and leave merging to the reviewer. @@ -427,7 +432,7 @@ python3 ~/reviews/fleet-ops/tools/reservation.py release uk-tl-bridge-2026-10-27 The historical pre-Budget pins and release hashes are in [J-certification](pilot/J-certification.md), and the source changes are in [J-upstream](pilot/J-upstream.md). The version bridge and all three rehearsal stages subsequently completed at execution head `b31137e`. The [completed verification receipt](../data/pilot/J-completed-verification.json) binds the completed artifacts; the [model-step bridge](../data/pilot/J-bridge-model-step.json), [certified bridge](../data/pilot/J-bridge-certified.json) and [GB coverage against DWP](../data/pilot/J-coverage-gb-dwp.json) retain only the permitted aggregate publications. They are **pre-Budget and not for quoting**. [MODEL_V2_PILOT](MODEL_V2_PILOT.md) records the numerical bridge and coverage interpretation. Rehearsal fiscal values remain private. Historical memory/runtime estimates above remain historical context. -The full build, standalone mean-path and single-scenario entry points, and real ageing validation enforce the exact certified client/model/core, primary pin and applicable certification evidence before model jobs. For the historical bundle that evidence is its released carrier manifest; the d1983 route requires #715's exact released carrier manifest and Microcosm artifact. The old d1727 qualification-ledger route remains closed and historical. Their provenance keeps that gate beside the behavioural Housing Benefit passport. Paired ageing additionally requires the source's exact certified packages, dataset SHA and manifests; the old pilot cannot supply that source. The version bridge verifies the retained pilot against its independently pinned historical content before reusing figures; an initially consistent but edited pilot is rejected, including under optimized Python. +The full build, standalone mean-path and single-scenario entry points, and real ageing validation enforce the exact certified client/model/core, primary pin and applicable certification evidence before model jobs. For the historical bundle that evidence is its released carrier manifest; the binding route requires the exact released carrier manifest of d2011's pair and its Microcosm artifact. The old d1727 qualification-ledger route remains closed and historical. Their provenance keeps that gate beside the behavioural Housing Benefit passport. Paired ageing additionally requires the source's exact certified packages, dataset SHA and manifests; the old pilot cannot supply that source. The version bridge verifies the retained pilot against its independently pinned historical content before reusing figures; an initially consistent but edited pilot is rejected, including under optimized Python. The successful unattended steps had these measured wrapper wall times. These are elapsed times for the complete step, including its own setup and cache handling; they are separate from the cold-job means used for planning. Attempts stopped by host admission have nonzero return codes and are excluded. diff --git a/docs/RESULTS_SCHEMA.md b/docs/RESULTS_SCHEMA.md index 245316fe..41860ed8 100644 --- a/docs/RESULTS_SCHEMA.md +++ b/docs/RESULTS_SCHEMA.md @@ -118,7 +118,7 @@ For d955(c), `expected_value` exists only if the **original monthly VAR(1) bootstrap primary** passes the binding [C2 screen](METHOD.md#model-v2-statutory-uncertainty-screen-pre-registration-c2). Its label is **model-conditional**. Passing alternatives are reported in `c2_outcome` individually; they never replace the primary or get averaged. If the primary fails, the build automatically records requested ruling `c`, effective ruling `a` and the failure reason, and omits `expected_value`. `scenario_envelope` remains beside the expected value after a pass and is the output after a failure: central path, OBR-wedge run, recorded historical last-decade replay (April 2017–2026, with its own model years) and paired ±0.5-point calendar-earnings mean paths from 2031. Each entry carries its saved full-run result or paired path-set estimate and recorded years, with no probability-interval claim. The historical replay is labelled with its period and is not combined into a future-year numerical range. -`release` marks an **interim pre-Budget file**, written by `triple-lock-build --pre-budget --uncertainty-ruling a` (scenarios only) or `c` (the published provisional, model-conditional expected value). The obsolete public (b) route is refused. `stage: "pre-budget"`, `interim: true`, `recorded_ruling: "c"`, `execution_ruling`, `forecast`/`forecast_source`, `cpi_period`, `april_2027_cpi_leg` and `binding_route` identify the interim vintage and replacement command. `budget_date` comes from `C2_BUDGET_DATE`; all rendered Budget dates use this release field. Ruling (c) first validates the full C2 handoff integrity contract, checks byte-identical primary and paired mean-path designs, and runs the full 40-slot Microcosm cross-check. `release.provisional: true`, `release.c2_dry_run`, `uncertainty_ruling`, `uncertainty_screen` and `expected_value.provenance` retain `run_kind: "dry_run"` and `expected_value_authorized: false` with the provisional labels. A pre-Budget file cannot authorize the binding route. A binding build requires the post-Budget inputs and handoff, writes no `release` block and removes the banner; primary failure omits expected value and retains scenarios. The historical pre-Budget file remains certified at 6.2.6 / 1.58.0. The pending d1727 binding bundle will certify EFRS 1.58.2 after the separate model and data bridge steps in REBUILD.md. +`release` marks an **interim pre-Budget file**, written by `triple-lock-build --pre-budget --uncertainty-ruling a` (scenarios only) or `c` (the published provisional, model-conditional expected value). The obsolete public (b) route is refused. `stage: "pre-budget"`, `interim: true`, `recorded_ruling: "c"`, `execution_ruling`, `forecast`/`forecast_source`, `cpi_period`, `april_2027_cpi_leg` and `binding_route` identify the interim vintage and replacement command. `budget_date` comes from `C2_BUDGET_DATE`; all rendered Budget dates use this release field. Ruling (c) first validates the full C2 handoff integrity contract, checks byte-identical primary and paired mean-path designs, and runs the full 40-slot Microcosm cross-check. `release.provisional: true`, `release.c2_dry_run`, `uncertainty_ruling`, `uncertainty_screen` and `expected_value.provenance` retain `run_kind: "dry_run"` and `expected_value_authorized: false` with the provisional labels. A pre-Budget file cannot authorize the binding route. A binding build requires the post-Budget inputs and handoff, writes no `release` block and removes the banner; primary failure omits expected value and retains scenarios. The historical pre-Budget file remains certified at 6.2.6 / 1.58.0. The binding build uses d2011's pair, certified by its release, policyengine.py 6.11.1 (Microcosm a6299504 primary, with Enhanced FRS 1.58.2 as the paired sensitivity), after the separate model and data bridge steps in REBUILD.md. `uncertainty_screen.rule_sha` is the committed C2 pre-registration `65343e2ee43a359f056ce5a027739509d32ab49f`; the handoff must match it before execution. Its `run_kind` is `binding`; a handoff labelled `dry_run` records a diagnostic run on pre-Budget inputs and cannot authorize an expected value. The one exception is the provisional pre-Budget (c) file above, which records the dry run's screen with `expected_value_authorized: false` and labels its expected value provisional. `expected_value_authorized` is true only for a binding primary pass; `authorization` records a dry run, a binding pass or the automatic fallback. `scoring_head` records the source commit used for scoring as provenance only; execution never requires that historic object. `rule_section_sha256` binds the exact frozen section bytes to the committed SHA256, and `scoring_inputs_sha256` binds the score table to the handoff's current code/data file hashes. Both survive a shallow clone or squash merge. `c1_failure` discloses that all five forms failed the original frozen C1 screen on test A with published earnings (1/6 terminal coverage, from the 2021 furlough base effect), the post-scoring rule change and the retained C1 record. `c2_outcome` retains the original-primary pass/fail and each alternative's result and failures. `c2_scores` carries both suspended and published summary tables, past-years percentiles, origins and legal-regime metadata beside that outcome; `score_table_sha256` identifies the complete handoff table, which also retains per-origin rows. The tables record legal exclusions and denominators. The rule-section hash, source snapshot and binding inputs support validation of the outcome and detect accidental changes; these hashes are audit evidence, not protection against jointly rewriting scores and their hashes. Suspended scoring, including its 5th–95th percentile past-years check, determines C2. Published-earnings scores, including their past-years percentile, remain sensitivities. Screen, rule SHA, C1 disclosure, C2 outcome and its quantitative sensitivity are carried in both results and provenance even when expected value is absent. diff --git a/docs/d2011-upstream.md b/docs/d2011-upstream.md index fb403b46..63c70e50 100644 --- a/docs/d2011-upstream.md +++ b/docs/d2011-upstream.md @@ -4,7 +4,7 @@ Read on 25 October 2026 from the released **policyengine-uk 2.151.0** wheel, com `data/certification/d2011-model-wheel.json` attests the wheel. Its archive SHA-256 equals PyPI's release metadata, and every one of its 3,176 RECORD members was checked for digest and size. The generator first reproduced the committed `d1983-model-wheel.json` byte for byte from PyPI's 2.147.2 wheel. No survey or Microcosm records were loaded. -`model_horizon.UPSTREAM_2151` and `UPSTREAM_2151_PARAMETERS` name the 2.151.0 bytes, and `check_upstream()` accepts that exact version alongside 2.125.1, 2.147.2 and the historical 2.153.4. The 2.147.2 absence guards apply to 2.151.0 too. Max ruled **d2011 = A** on 25 October. **This change does not move the binding pins:** #715's pair (2.147.2 / a133f37a) stays pinned in `datasets.py`, `pyproject.toml` and the lock until a certified policyengine.py release carries the chosen pair (#39). +`model_horizon.UPSTREAM_2151` and `UPSTREAM_2151_PARAMETERS` name the 2.151.0 bytes, and `check_upstream()` accepts that exact version alongside 2.125.1, 2.147.2 and the historical 2.153.4. The 2.147.2 absence guards apply to 2.151.0 too. Max ruled **d2011 = A** on 25 October, so the binding pins now name this pair: UK 2.151.0 in `pyproject.toml` and the lock, and Microcosm a6299504 in `datasets.py` and `config.PRIMARY_DATASET`. Certification stayed closed until a policyengine.py release carried the pair: policyengine.py 6.11.1 did on 27 October ([#720](https://github.com/PolicyEngine/policyengine.py/pull/720)), and certification requires exactly that installed bundle. #715's pair (2.147.2 / a133f37a) stays registered as d2011's fallback. ## Every changed guard diff --git a/pyproject.toml b/pyproject.toml index 271b468a..95602616 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,13 +18,13 @@ dependencies = [ ] [project.optional-dependencies] -# #715's candidate UK model/core, explicitly pinned while its carrier release is pending. +# d2011's pair's UK model/core (microcosm#1396's uv.lock), pinned explicitly beside its released carrier. # The engine constructs policyengine_uk directly and materializes its own pinned data. -# Use the released carrier without [uk]: 6.10.1's extra pins the previous model. -# Once #715 is released, update the carrier version and manifest digest in datasets.py. +# The carrier is installed without [uk]: an extra may pin another model. scripts/fill_release_identities.py sets +# its version from the release, with the identities in datasets.py; then re-resolve the lock (docs/REBUILD.md). uk = [ - "policyengine==6.10.1", - "policyengine-uk==2.147.2", + "policyengine==6.11.1", + "policyengine-uk==2.151.0", "policyengine-core==3.33.3", "huggingface-hub>=1.0", "openpyxl>=3.1", diff --git a/requirements-lock.txt b/requirements-lock.txt index c9d88fdd..62711f2d 100644 --- a/requirements-lock.txt +++ b/requirements-lock.txt @@ -1,6 +1,6 @@ -# d1983 B: #715 candidate model/core, pending exact released carrier certification. -# Base policyengine carrier deliberately has no [uk] extra (its released UK pin differs). -# Dataset and future release-manifest identity are pinned in triple_lock/datasets.py. +# d2011 A: microcosm#1396's model/core, with the carrier datasets.py pins (policyengine.py 6.11.1, the pair's release). +# Base policyengine carrier deliberately has no [uk] extra (an extra may pin another model). +# Dataset and release-manifest identities are pinned in triple_lock/datasets.py. # Historical receipts retain their original model/data identities. annotated-types==0.8.0 anyio==4.15.1 @@ -53,9 +53,9 @@ parso==0.8.7 pexpect==4.9.0 plotly==5.24.1 pluggy==1.6.0 -policyengine==6.10.1 +policyengine==6.11.1 policyengine-core==3.33.3 -policyengine-uk==2.147.2 +policyengine-uk==2.151.0 prompt-toolkit==3.0.53 psutil==6.1.1 ptyprocess==0.7.0 diff --git a/scripts/binding_bridge.py b/scripts/binding_bridge.py index 1a612471..2bf4c0a9 100644 --- a/scripts/binding_bridge.py +++ b/scripts/binding_bridge.py @@ -1,4 +1,4 @@ -"""Pin validation and aggregate comparisons for the pending binding bridge. +"""Pin validation and aggregate comparisons for the binding bridge. An endpoint pins the installed package versions and exact dataset bytes. The model step holds Enhanced FRS fixed and is uncertified; the data step holds @@ -36,7 +36,7 @@ def validate_endpoint(endpoint): def validate_configuration(configuration): if configuration.get("status") != "ready": - raise ValueError("binding bundle is pending: wait for policyengine.py #715 attribution, review and release; " + raise ValueError("binding bundle is pending: wait for the policyengine.py release carrying d2011's pair; " "fill its released carrier version and manifest SHA-256, then mark the bridge configuration ready") if configuration.get("schema_version") != 1 or set(configuration.get("steps", {})) != {"model", "data"}: raise ValueError("bridge configuration requires model and data steps") diff --git a/scripts/compare_hb_link.py b/scripts/compare_hb_link.py index 6d8d09f6..0c4bc8eb 100644 --- a/scripts/compare_hb_link.py +++ b/scripts/compare_hb_link.py @@ -1,7 +1,7 @@ """Full central-path comparisons of HB and Savings Credit projection conventions. Run only through the shared-host heavy_run.sh admission wrapper. On the binding -UK 2.147.2 bundle, d1753 keeps the installed lagged-calendar-CPI projections as +UK 2.151.0 bundle, d1753 keeps the installed lagged-calendar-CPI projections as the headline. The default linked alternative moves both pension-age HB cohorts and Savings Credit thresholds with the path guarantee, preserving the enacted 2026 component grown by non-negative statutory September CPI. The cpi selector diff --git a/scripts/fill_release_identities.py b/scripts/fill_release_identities.py new file mode 100644 index 00000000..65416a88 --- /dev/null +++ b/scripts/fill_release_identities.py @@ -0,0 +1,325 @@ +"""Check a policyengine.py release against d2011's pinned pair, then fill the identities only it can supply. + + .venv/bin/python scripts/fill_release_identities.py --pr N --release TAG [--target fallback] [--apply] + .venv/bin/python scripts/fill_release_identities.py --pr N --manifest FILE [--target fallback] + +The UK bundle manifest is read at the release tag (the release workflow's version commit rewrites it after the +certifying merge, so the merge commit's bytes are not the release's), or from ``--manifest FILE``, such as the +installed wheel's. Every identity datasets.py already pins is compared the way +``datasets._certified_by_installed_bundle`` will compare the installed release: carrier version, model and core +versions, model wheel SHA-256, build id, release and source manifest URIs, the certified artifact, region routing +(national to the pinned file; constituency and local authority to the local-area file) and the local-area cut. + +The local-area cut's own release manifest is read at its tag on the Hub (``HUGGING_FACE_TOKEN``), or from +``--regional-manifest FILE``. Its build must name the cut's tag and have run the cut's model (2.151.0 for the +binding pair, whose bundle must carry no ``regional_certification``; 2.132.6 for the fallback, on #699's accepted +mismatch) on core 3.33.3, and its local-area artifact must be the file the bundle routes to. The fallback's cut is +pinned already; the binding cut's file, tag, SHA-256 and manifest SHA-256 come from the release and are filled. + +With ``--release``, the certifying pull request must be merged, its merge commit must be in the tag, and the tag's +manifest must be, byte for byte, what the release workflow writes from the merge commit's: the carrier version +fields, each country's bundle id, the install requirements and the citation are rewritten and ``source`` is dropped; +nothing else changes. The release is then exactly what the certifying review merged. Any difference exits 1 before +anything is written. + +``--apply`` (with ``--release`` only) writes the target's identities: carrier version, manifest SHA-256, certifying +pull request and merge commit, and for the binding target its local-area cut. The binding target also writes the +carrier pin in pyproject.toml and requirements-lock.txt and data/binding-bridge.json, the pending configuration +filled and marked ready. The fallback target writes only its datasets identities: its model pin, primary and bridge +endpoints move in their own reviewed commit. Every file is checked before any is written. Nothing is installed and +no data is loaded. Afterwards: re-resolve the lock for the new carrier, install it, rerun this with ``--manifest`` +on the installed wheel's manifest, and run check_upstream(), both preflights and the bridge's --validate-only. +""" + +import argparse +import base64 +from copy import deepcopy +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) +REPOSITORY = "PolicyEngine/policyengine.py" +MANIFEST = "src/policyengine/data/bundle/manifest.json" +TARGETS = {"binding": ("microcosm_uk_2024_25@a6299504", "BINDING"), + "fallback": ("microcosm_uk_2024_25@a133f37a", "FALLBACK")} +BUNDLE_IDENTITIES = ("BUNDLE_VERSION", "BUNDLE_MANIFEST_SHA256", "CERTIFICATION_URL", "CERTIFICATION_COMMIT") +LOCAL_AREA_IDENTITIES = ("LOCAL_AREA_PATH", "LOCAL_AREA_REVISION", "LOCAL_AREA_SHA256", "LOCAL_AREA_MANIFEST_SHA256") +IDENTITIES = {"BINDING": BUNDLE_IDENTITIES + LOCAL_AREA_IDENTITIES, "FALLBACK": BUNDLE_IDENTITIES} +LOCAL_AREA_ROLE = "non_default_local_area" + + +def github(path): + return json.loads(subprocess.run(["gh", "api", path], check=True, capture_output=True, text=True).stdout) + + +def github_file(path, ref): + """A file's exact bytes at a ref of policyengine.py.""" + return base64.b64decode(github(f"repos/{REPOSITORY}/contents/{path}?ref={ref}")["content"]) + + +def hub_file(local): + """The local-area cut's release manifest, at the cut's own tag.""" + from huggingface_hub import hf_hub_download + from triple_lock.datasets import LOCAL_AREA_RELEASE_MANIFEST + + return Path(hf_hub_download(local["repo_id"], LOCAL_AREA_RELEASE_MANIFEST, revision=local["revision"], + repo_type=local["repo_type"], token=os.environ.get("HUGGING_FACE_TOKEN"))).read_bytes() + + +def local_area_entry(release, path): + """The release's one datasets entry for the local-area file ({} unless exactly one names it).""" + entries = [entry for entry in release.get("datasets", {}).values() + if isinstance(entry, dict) and entry.get("path") == path] + return entries[0] if len(entries) == 1 else {} + + +def released_local_area(release): + """The local-area file the release routes constituency runs to, with its tag and SHA-256.""" + path = (release["region_datasets"].get("constituency") or {}).get("path_template") + entry = local_area_entry(release, path) + return {"LOCAL_AREA_PATH": path, "LOCAL_AREA_REVISION": entry.get("revision"), + "LOCAL_AREA_SHA256": entry.get("sha256")} + + +def checks(manifest, spec, local, regional_raw): + """[(label, pinned, released)] for every identity the registry pins, and for the local-area cut ``local`` (the + pinned cut, or for a pending binding cut the one the release names) against its own release manifest's bytes.""" + from triple_lock.datasets import _local_area_routed, area_routed, certified_core + + certification = spec["bundle_certification"] + release = manifest["data_releases"]["uk"] + artifact = release["certified_data_artifact"] + version = manifest["policyengine_version"] + model = certification["model_version"] + entry = local_area_entry(release, local["path"]) + regional = json.loads(regional_raw) + build = regional.get("build", {}) + cut_artifacts = [item for item in regional.get("artifacts", {}).values() + if isinstance(item, dict) and item.get("path") == local["path"]] + certificate = release.get("regional_certification") + if local["model_basis"] == "built_with": + pinned_certificate = None # both halves on the pair's own model + else: # policyengine.py #699's record of the reviewed mismatch + pinned_certificate = {"compatibility_basis": local["model_basis"], "certified_for_model_version": model, + "built_with_model_version": local["built_with_model_version"], + "regional_manifest_uri": local["source_manifest_uri"], + "regional_manifest_sha256": local["manifest_sha256"], "area_receipts": "uncertified"} + if isinstance(certificate, dict): + certificate = {key: certificate.get(key) for key in pinned_certificate} + return [ + ("UK release carrier version", version, release["policyengine_version"]), + ("bundle id", f"uk-{version}", release["bundle_id"]), + ("core version", certification["core_version"], manifest["packages"]["policyengine-core"]["version"]), + ("core version certified for", certification["core_version"], certified_core(manifest)), + ("model version", model, release["model_package"]["version"]), + ("model version certified for", model, release["certification"]["certified_for_model_version"]), + ("model wheel SHA-256", certification["model_wheel_sha256"], release["model_package"]["sha256"]), + ("release build id", spec["build_id"], release["build_id"]), + ("artifact build id", spec["build_id"], artifact["build_id"]), + ("release manifest URI", spec["release_manifest"]["uri"], release["release_manifest_uri"]), + ("source manifest URI", spec["release_manifest"]["source_uri"], release["source_manifest_uri"]), + ("artifact dataset", spec["logical_name"], artifact["dataset"]), + ("artifact SHA-256", spec["sha256"], artifact["sha256"]), + ("artifact URI", f"hf://{spec['repo_id']}/{spec['path']}@{spec['revision']}", artifact["uri"]), + ("artifact data package", {"name": spec["data_package"], "version": spec["data_version"]}, + artifact["data_package"]), + # Exactly what the installed check compares: national to the pinned file, both area types to one file. + ("region routing", area_routed(local["path"]), release["region_datasets"]), + ("local-area datasets entry", {key: local[key] for key in ("repo_id", "repo_type", "revision", "sha256")}, + {key: entry.get(key, local["repo_type"] if key == "repo_type" else None) + for key in ("repo_id", "repo_type", "revision", "sha256")}), + ("regional release kind", "local_area", release.get("regional_release_kind")), + ("regional release manifest URI", local["release_manifest_uri"], release.get("regional_release_manifest_uri")), + ("regional source manifest URI", local["source_manifest_uri"], release.get("regional_source_manifest_uri")), + ("regional source manifest SHA-256", local["manifest_sha256"], + release.get("regional_source_manifest_sha256", None if pinned_certificate else local["manifest_sha256"])), + ("regional certification", pinned_certificate, certificate), + ("local-area release manifest SHA-256", local["manifest_sha256"], hashlib.sha256(regional_raw).hexdigest()), + ("local-area cut tag", local["revision"], build.get("cut_tag")), + ("local-area build model", local["built_with_model_version"], + build.get("built_with_model_package", {}).get("version")), + ("local-area build core", certification["core_version"], + build.get("built_with_core_package", {}).get("version")), + ("local-area dataset role", LOCAL_AREA_ROLE, regional.get("dataset_role")), + ("local-area artifact", [{"kind": "microdata", "path": local["path"], "repo_id": local["repo_id"], + "revision": local["revision"], "sha256": local["sha256"]}], + [{key: item.get(key) for key in ("kind", "path", "repo_id", "revision", "sha256")} + for item in cut_artifacts]), + # The function provenance calls, so the script and the installed check cannot disagree. + ("installed check routes to this cut", True, _local_area_routed(release, local, model)), + ] + + +def as_released(merged, version): + """The manifest the release workflow writes for ``version`` from the certifying merge's ``merged``, as at + policyengine.py 6.11.0: .github/bump_version.py's sync_bundle_versions (the carrier version fields, each country's + bundle id), then scripts/generate_bundle_artifacts.py's normalized_manifest (each package's install requirement, + the citation, no ``source``). Nothing else is rewritten. None where sync_bundle_versions or normalized_manifest + would fail on ``merged``'s structure; the workflow's validation and pre-versioning checks are not replayed, since + the tag must equal this rewrite in any case.""" + if not (isinstance(merged, dict) and {"bundle_version", "policyengine_version", "packages"} <= merged.keys()): + return None + released = deepcopy(merged) + try: + released["bundle_version"] = released["policyengine_version"] = version + released["packages"]["policyengine"]["version"] = version + for country, data_release in released.get("data_releases", {}).items(): + if isinstance(data_release, dict): + data_release.update(policyengine_version=version, bundle_id=f"{country}-{version}") + for package in released["packages"].values(): + package["install_requirement"] = (f"{package['name']}=={package['version']}" + + (f"; {package['markers']}" if package.get("markers") else "")) + except (AttributeError, KeyError, TypeError): + return None + released.pop("source", None) + released["citation"] = {"title": f"PolicyEngine bundle {version}", "version": version, "type": "software-bundle", + "publisher": "PolicyEngine"} + return released + + +def manifest_text(manifest): + """The bytes the release workflow writes for a manifest.""" + return (json.dumps(manifest, indent=2, sort_keys=True) + "\n").encode() + + +def certifying_checks(pull_request, tag, raw): + """[(label, pinned, released)]: the pull request merged, into this tag, whose manifest bytes ``raw`` are exactly + what the release workflow writes from the merge's.""" + commit = pull_request.get("merge_commit_sha") if pull_request.get("merged_at") else None + rows = [("certifying pull request merged", True, commit is not None)] + if commit is None: + return rows + status = github(f"repos/{REPOSITORY}/compare/{commit}...{tag}")["status"] + expected = as_released(json.loads(github_file(MANIFEST, commit)), json.loads(raw)["policyengine_version"]) + return rows + [ + ("merge commit is in the release", True, status in ("ahead", "identical")), + ("release manifest is the workflow's rewrite of the merge's", True, + expected is not None and manifest_text(expected) == raw), + ] + + +def identities(raw, manifest, pull_request, prefix, regional_raw): + """The values only the release supplies, from the manifest's exact bytes, the pull request and (binding) the + local-area cut it names. An unmerged pull request has no certifying commit (None), so its values can be + printed but never applied.""" + values = {"BUNDLE_VERSION": manifest["policyengine_version"], + "BUNDLE_MANIFEST_SHA256": hashlib.sha256(raw).hexdigest(), + "CERTIFICATION_URL": pull_request["html_url"], + "CERTIFICATION_COMMIT": pull_request["merge_commit_sha"] if pull_request.get("merged_at") else None} + if prefix == "BINDING": + values.update(released_local_area(manifest["data_releases"]["uk"]), + LOCAL_AREA_MANIFEST_SHA256=hashlib.sha256(regional_raw).hexdigest()) + return values + + +def _replace_once(text, pattern, replacement, where): + """``replacement`` is a function of the match, so no value is read as a template.""" + text, count = re.subn(pattern, replacement, text, flags=re.M) + if count != 1: + raise ValueError(f"{where}: expected exactly one match for {pattern!r}: nothing is written") + return text + + +def fill(root, prefix, values, reviewed_head): + """Write the identities; the binding target also writes its carrier pins and the ready bridge configuration. + Every file is built and checked first, so a refusal writes nothing.""" + missing = [name for name in IDENTITIES[prefix] if not values.get(name)] + if missing: + raise ValueError(f"the release does not supply {missing}: nothing is written") + path = root / "src/triple_lock/datasets.py" + text = path.read_text() + for name in IDENTITIES[prefix]: + assignment = f"{prefix}_{name} = {json.dumps(values[name])}" + text = _replace_once(text, rf"^{prefix}_{name} = None$", lambda _, line=assignment: line, path) + writes = {path: text} + if prefix == "BINDING": + version = values["BUNDLE_VERSION"] + for path in (root / "pyproject.toml", root / "requirements-lock.txt"): + writes[path] = _replace_once(path.read_text(), r'^(\s*"?policyengine==)[0-9][^",\s]*', + lambda match: match.group(1) + version, path) + configuration = json.loads((root / "data/binding-bridge.pending.json").read_text()) + for step, endpoint in (("model", "to"), ("data", "from"), ("data", "to")): + packages = configuration["steps"][step][endpoint]["packages"] + if packages["policyengine"] is not None: + raise ValueError(f"the pending {step}/{endpoint} endpoint already names a carrier: nothing is written") + packages["policyengine"] = version + configuration["status"] = "ready" + configuration["pending_release"].update( + pull_request=values["CERTIFICATION_URL"], reviewed_head=reviewed_head, policyengine_version=version, + bundle_manifest_sha256=values["BUNDLE_MANIFEST_SHA256"]) + writes[root / "data/binding-bridge.json"] = json.dumps(configuration, indent=2) + "\n" + for path, text in writes.items(): + path.write_text(text) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--pr", type=int, required=True, help="the merged policyengine.py PR certifying the pair") + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--release", help="the policyengine.py release tag carrying it") + source.add_argument("--manifest", type=Path, help="that release's manifest file, e.g. the installed wheel's") + parser.add_argument("--regional-manifest", type=Path, + help="the local-area cut's release_manifest.json (default: read it at the cut's tag)") + parser.add_argument("--target", choices=sorted(TARGETS), default="binding") + parser.add_argument("--apply", action="store_true", + help="with --release: write the identities once every check passes") + args = parser.parse_args(argv) + if args.apply and not args.release: + parser.error("--apply needs --release: only the tag's checks authenticate what it writes") + + from triple_lock import datasets + + name, prefix = TARGETS[args.target] + spec = datasets.DATASETS[name] + raw = args.manifest.read_bytes() if args.manifest else github_file(MANIFEST, args.release) + manifest = json.loads(raw) + if args.release and manifest["policyengine_version"] != args.release: + raise SystemExit(f"release {args.release}'s manifest names policyengine {manifest['policyengine_version']}") + local = spec["local_area"] + if any(local[key] is None for key in ("path", "revision", "sha256", "manifest_sha256")): + # The binding cut, pending: check the one the release names, with its release manifest's digest. + supplied = released_local_area(manifest["data_releases"]["uk"]) + local = datasets.local_area_cut(supplied["LOCAL_AREA_PATH"], supplied["LOCAL_AREA_REVISION"], + supplied["LOCAL_AREA_SHA256"], None, local["built_with_model_version"], + local["model_basis"]) + if local["revision"] is None and not args.regional_manifest: + raise SystemExit("refused: the release names no single local-area datasets entry to read the cut from") + regional_raw = args.regional_manifest.read_bytes() if args.regional_manifest else hub_file(local) + if local["manifest_sha256"] is None: + local["manifest_sha256"] = hashlib.sha256(regional_raw).hexdigest() + rows = checks(manifest, spec, local, regional_raw) + pull_request = github(f"repos/{REPOSITORY}/pulls/{args.pr}") + if args.release: + rows += certifying_checks(pull_request, args.release, raw) + values = identities(raw, manifest, pull_request, prefix, regional_raw) + # Identities already filled (a rerun on the installed wheel's manifest) must equal what this release supplies. + rows += [(f"filled {prefix}_{key}", getattr(datasets, f"{prefix}_{key}"), value) for key, value in values.items() + if getattr(datasets, f"{prefix}_{key}") is not None] + width = max(len(label) for label, _, _ in rows) + for label, pinned, released in rows: + print(f"{'ok ' if pinned == released else 'FAIL'} {label:<{width}} {json.dumps(released)}" + + ("" if pinned == released else f" (pinned {json.dumps(pinned)})")) + print(json.dumps({"dataset": name, **{f"{prefix}_{key}": value for key, value in values.items()}, + "reviewed_head": pull_request["head"]["sha"]}, indent=2)) + if any(pinned != released for _, pinned, released in rows): + print(f"refused: the release differs from the pinned {name} pair", file=sys.stderr) + return 1 + if args.apply: + if any(label.startswith("filled ") for label, _, _ in rows): + raise SystemExit(f"{prefix} identities are already filled: rerun without --apply to check them") + fill(ROOT, prefix, values, pull_request["head"]["sha"]) + print("applied: review the diff; re-resolve requirements-lock.txt for the new carrier (docs/REBUILD.md); " + "install it; rerun this with --manifest on the installed wheel's manifest; run check_upstream(), " + "both preflights and the bridge's --validate-only") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/run_model_v2_j_bridge.py b/scripts/run_model_v2_j_bridge.py index 4b8ee6df..e2522e0b 100644 --- a/scripts/run_model_v2_j_bridge.py +++ b/scripts/run_model_v2_j_bridge.py @@ -589,7 +589,7 @@ def binding_main(args): Model/to compares against the exact committed 6.2.6 receipt. Data/to must reuse model/to, so the intermediate endpoint cannot silently be substituted. - The unreleased bundle is deliberately not guessed or installed here. + The released bundle is deliberately not guessed or installed here: it must be the installed lock. """ workspace = Path.cwd().resolve() destination = args.out.resolve() diff --git a/src/triple_lock/config.py b/src/triple_lock/config.py index 6e1bec9c..fa07951f 100644 --- a/src/triple_lock/config.py +++ b/src/triple_lock/config.py @@ -191,10 +191,11 @@ CPI_AUG_SEP_FIRST_YEAR = 1997 # Datasets (datasets.DATASETS: each pinned to a revision and a SHA-256). The -# binding primary is the exact Microcosm UK Budget cut proposed by policyengine.py #715; -# publication remains blocked until the certified carrier is released. Enhanced FRS -# 1.58.2 is the paired sensitivity (d1983 = B supersedes d1727). -PRIMARY_DATASET = "microcosm_uk_2024_25@a133f37a" +# binding primary is the Microcosm UK Budget national cut d2011 certifies (= A: +# a6299504, microcosm#1396, on policyengine-uk 2.151.0), released in +# policyengine.py 6.11.1; publication requires exactly that installed bundle. +# Enhanced FRS 1.58.2 is the paired sensitivity (d1983 = B supersedes d1727). +PRIMARY_DATASET = "microcosm_uk_2024_25@a6299504" SENSITIVITY_DATASET = "enhanced_frs_2024_25@1.58.2" # Outputs. diff --git a/src/triple_lock/datasets.py b/src/triple_lock/datasets.py index 5ab405e7..6302c7be 100644 --- a/src/triple_lock/datasets.py +++ b/src/triple_lock/datasets.py @@ -1,8 +1,14 @@ """Pinned data for the binding Microcosm primary and Enhanced FRS sensitivity. -The #715 UK Budget candidate is Microcosm a133f37a / UK 2.147.2 / core -3.33.3, national only. Its carrier release is pending: provenance fails closed -until its released version and installed manifest digest are pinned below. +d2011 = A (Max, 25 October 2026): the binding primary is the 2.151.0 national +and local-area pair built at microcosm#1396, Microcosm a6299504 / UK 2.151.0 / +core 3.33.3, released in policyengine.py 6.11.1 on 27 October 2026 and +certified by policyengine.py#720. Provenance requires the released version, +installed manifest digest, certifying pull request and commit, and local-area +cut pinned below. #715's a133f37a / UK 2.147.2 pair, d1983's first binding +pair, was d2011's fallback with policyengine.py #699's area routing to the +19 October local-area cut, which is pinned; the pair shipped before d2011's +deadline, so the fallback was not taken, and its bundle identities stay pending. Historical Enhanced FRS and Microcosm artifacts retain their original pins. Materialization checks the exact artifact SHA-256 before every use (cached while unchanged), with one shared copy in .cache/datasets per artifact. @@ -27,15 +33,21 @@ "certification_url": "https://github.com/PolicyEngine/policyengine.py/pull/555", "certification_commit": "37d25c423e9541b4ca86adbdd8687738b49ab8ec", } -# The only unknown certification identities: fill from the release carrying #715. -# The candidate head's 6.10.1 is NOT that release (the published version has older data). -BINDING_BUNDLE_VERSION = None -BINDING_BUNDLE_MANIFEST_SHA256 = None -CERTIFIED_BUNDLE = { - "bundle_package": "policyengine", "bundle_version": BINDING_BUNDLE_VERSION, - "bundle_id": f"uk-{BINDING_BUNDLE_VERSION}", "model_version": "2.147.2", "core_version": "3.33.3", +# #715's pair, d1983's first binding pair, is d2011's fallback if the 2.151.0 pair is not certified by Tue 27 Oct +# 18:00Z: a133f37a's national file with policyengine.py #699's area routing (the 19 Oct dense local-area cut +# microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe, built on 2.132.6, on an accepted model mismatch; pinned in +# FALLBACK_LOCAL_AREA below). #715's candidate head e02d41bc routes national only, and its 6.10.1 is NOT the release. +# Unknown until a fallback release: its version and installed manifest digest, and the pull request and commit that +# certify it. Each fails closed while None. +FALLBACK_BUNDLE_VERSION = None +FALLBACK_BUNDLE_MANIFEST_SHA256 = None +FALLBACK_CERTIFICATION_URL = None +FALLBACK_CERTIFICATION_COMMIT = None +BUNDLE_715 = { + "bundle_package": "policyengine", "bundle_version": FALLBACK_BUNDLE_VERSION, + "bundle_id": f"uk-{FALLBACK_BUNDLE_VERSION}", "model_version": "2.147.2", "core_version": "3.33.3", "manifest_path": "policyengine/data/bundle/manifest.json", - "manifest_sha256": BINDING_BUNDLE_MANIFEST_SHA256, + "manifest_sha256": FALLBACK_BUNDLE_MANIFEST_SHA256, "model_wheel_sha256": "4b48ab1008b00610abc38192927ac9d4bb22037b6bbd9578926c2899e639f972", "wheels": { "policyengine-uk": { @@ -49,18 +61,110 @@ "wheel_sha256": "5847d9938412095fe9848c2ce535838a7dc8fac9238d8fd9e6c9d0a0648e0888", }, }, - "certification_url": "https://github.com/PolicyEngine/policyengine.py/pull/715", - "certification_commit": "e02d41bceab57756373b2dc3bcf78635b856ec81", + "certification_url": FALLBACK_CERTIFICATION_URL, + "certification_commit": FALLBACK_CERTIFICATION_COMMIT, } -MICROCOSM_REVISION = "microcosm-uk-2024-25-national-20261023T075039Z-a133f37a" -MICROCOSM_RELEASE_MANIFEST = { - "revision": MICROCOSM_REVISION, - "path": "releases/microcosm-uk-2024-25-national/release_manifest.json", - "uri": f"https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/{MICROCOSM_REVISION}/" - "releases/microcosm-uk-2024-25-national/release_manifest.json", - "source_uri": f"hf://dataset/policyengine/populace-uk-private@{MICROCOSM_REVISION}/" - "releases/microcosm-uk-2024-25-national/release_manifest.json", + + +def microcosm_release_manifest(revision): + """Where a Microcosm UK national release tag keeps its release manifest.""" + return { + "revision": revision, + "path": "releases/microcosm-uk-2024-25-national/release_manifest.json", + "uri": f"https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/{revision}/" + "releases/microcosm-uk-2024-25-national/release_manifest.json", + "source_uri": f"hf://dataset/policyengine/populace-uk-private@{revision}/" + "releases/microcosm-uk-2024-25-national/release_manifest.json", + } + + +LOCAL_AREA_REPO = "policyengine/populace-uk-private" +LOCAL_AREA_RELEASE_MANIFEST = "releases/microcosm-uk-2024-25-dense/release_manifest.json" + + +def local_area_cut(path, revision, sha256, manifest_sha256, built_with_model_version, model_basis): + """The local-area cut a d2011 pair's constituency and local-authority runs load: its file, release tag and + SHA-256; where the bundle names its release manifest, and the SHA-256 of those bytes; the model its build ran; + and the basis on which the bundle routes to it. "built_with": the cut's build ran the pair's own model, so the + bundle carries no regional_certification. "accepted_model_mismatch": policyengine.py #699's + regional_certification records the reviewed mismatch. Any pending (None) part certifies nothing.""" + uri = lambda form: None if revision is None else form.format(repo=LOCAL_AREA_REPO, revision=revision, + manifest=LOCAL_AREA_RELEASE_MANIFEST) + return {"path": path, "repo_id": LOCAL_AREA_REPO, "repo_type": "dataset", "revision": revision, "sha256": sha256, + "release_manifest_uri": uri("https://huggingface.co/datasets/{repo}/resolve/{revision}/{manifest}"), + "source_manifest_uri": uri("hf://dataset/{repo}@{revision}/{manifest}"), + "manifest_sha256": manifest_sha256, "built_with_model_version": built_with_model_version, + "model_basis": model_basis} + + +def area_routed(local_area_path): + """A UK Budget release's region routing: the national file serves national runs, and policyengine.py routes + constituency and local-authority runs to the local-area file (provenance/certification.py: + LOCAL_AREA_REGION_TYPES["uk"]). A pending (None) file certifies nothing.""" + return {"national": {"path_template": "microcosm_uk_2024_25.h5"}, + "constituency": {"path_template": local_area_path}, + "local_authority": {"path_template": local_area_path}} + + +MICROCOSM_REVISION_A133 = "microcosm-uk-2024-25-national-20261023T075039Z-a133f37a" +MICROCOSM_RELEASE_MANIFEST_A133 = microcosm_release_manifest(MICROCOSM_REVISION_A133) +# The release manifest at the cut's tag is 61,723 bytes; its build (built_with_model_package 2.132.6, core 3.33.3) +# names the tag as its cut_tag. policyengine.py 6.11.0 and #699's Budget route both route to this file. +FALLBACK_LOCAL_AREA = local_area_cut( + "microcosm_uk_2024_25_dense.h5", "microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe", + "8e10e130d0260e0b52e2e29902f0502ebdff76a14a3e57d2c876bf4cd8db5872", + "b9dc7cae358b9c965935f83721508594f9fb56a35911ffb0f9216ecb6a799561", "2.132.6", "accepted_model_mismatch") +FALLBACK_REGION_DATASETS = area_routed(FALLBACK_LOCAL_AREA["path"]) + +# d2011's pair, as only the policyengine.py release that carries it (6.11.1) could supply them: that release's +# version and installed manifest digest, the pull request and commit that certify it, and the 2.151.0 local-area cut +# its constituency and local-authority runs load (file, release tag, SHA-256, and the SHA-256 of the cut's release +# manifest). scripts/fill_release_identities.py filled them from the release; each fails closed while None. +BINDING_BUNDLE_VERSION = "6.11.1" +BINDING_BUNDLE_MANIFEST_SHA256 = "ab60903b390e7a765ea6558ec5e2d8fa5aede06b6f735fbf4e4c96109ffd1617" +BINDING_CERTIFICATION_URL = "https://github.com/PolicyEngine/policyengine.py/pull/720" +BINDING_CERTIFICATION_COMMIT = "610ebc42586b7358c41afe4fabec7c67adf17205" +BINDING_LOCAL_AREA_PATH = "microcosm_uk_2024_25_dense.h5" +BINDING_LOCAL_AREA_REVISION = "microcosm-uk-2024-25-dense-20261026T105309Z-c9df4b86" +BINDING_LOCAL_AREA_SHA256 = "cb3712f25b76109ad27b7b0cdf5dee78c55d3b7f55ce7bf207ef1a191da2ae9a" +BINDING_LOCAL_AREA_MANIFEST_SHA256 = "30be47545efd2aa320e30f8d04240cf345aec105fa1d788334770bc4e17c583e" +# The base carrier the preparatory lock installed while BINDING_BUNDLE_VERSION was pending (its [uk] extra pins its +# own previous model, so the lock omits [uk]); the fill replaced it with the release in pyproject.toml and the lock. +# Tests restore it to rebuild the pending state. +PREPARATORY_CARRIER = "6.10.1" +CERTIFIED_BUNDLE = { + "bundle_package": "policyengine", "bundle_version": BINDING_BUNDLE_VERSION, + "bundle_id": f"uk-{BINDING_BUNDLE_VERSION}", "model_version": "2.151.0", "core_version": "3.33.3", + "manifest_path": "policyengine/data/bundle/manifest.json", + "manifest_sha256": BINDING_BUNDLE_MANIFEST_SHA256, + "model_wheel_sha256": "5722f55aca3eb88e1a228e95e1a57e3bd70a4d530a5a000991565ebd8955ce35", + "wheels": { + "policyengine-uk": { + "manifest_path": "data/certification/d2011-model-wheel.json", + "manifest_sha256": "a67bd0a5afad28def6327f3cb2f056ef54c5e5da31eb03f4003d58d7ca5edc25", + "wheel_sha256": "5722f55aca3eb88e1a228e95e1a57e3bd70a4d530a5a000991565ebd8955ce35", + }, + "policyengine-core": { + "manifest_path": "data/certification/d1983-core-wheel.json", + "manifest_sha256": "dfbd18d2553e84b0e302ea22a637d4b66b6214623be7ec67874fbbe5fac42867", + "wheel_sha256": "5847d9938412095fe9848c2ce535838a7dc8fac9238d8fd9e6c9d0a0648e0888", + }, + }, + "certification_url": BINDING_CERTIFICATION_URL, + "certification_commit": BINDING_CERTIFICATION_COMMIT, } +# d2011's pairs, binding first: the only primaries certified_bundle_preflight admits. The fallback becomes the +# primary only in its own reviewed commit (docs/REBUILD.md, "Fallback"). +D2011_PAIRS = ("microcosm_uk_2024_25@a6299504", "microcosm_uk_2024_25@a133f37a") +# The national's release tag, from the staged attempt uk-frs-calibration-attempt-20261025T091414Z-a6299504 +# (staged_manifest.json: git_commit 67ee2ddf3, clean). The tag exists on the Hub with the pinned file (checked 26 +# October: LFS SHA-256 b96a8cd6..., 342,348,841 bytes); materialization still verifies the bytes before every use, and +# the bundle must name the same artifact URI. +MICROCOSM_REVISION = "microcosm-uk-2024-25-national-20261025T091414Z-a6299504" +MICROCOSM_RELEASE_MANIFEST = microcosm_release_manifest(MICROCOSM_REVISION) +BINDING_LOCAL_AREA = local_area_cut(BINDING_LOCAL_AREA_PATH, BINDING_LOCAL_AREA_REVISION, BINDING_LOCAL_AREA_SHA256, + BINDING_LOCAL_AREA_MANIFEST_SHA256, "2.151.0", "built_with") +BINDING_REGION_DATASETS = area_routed(BINDING_LOCAL_AREA_PATH) RELEASE_MANIFEST_1_58_0 = { "revision": "fdeba38517bd86067f34c0a5d8ed8923efb3b10a", "path": "releases/1.58.0/release_manifest.json", @@ -79,17 +183,40 @@ # the file (for other policyengine-uk versions), "built_with" the policyengine-uk version its data build ran, # "ageing_anchor" the year static ageing anchors its weights to (the data build's calibration year where it has one). DATASETS = { - "microcosm_uk_2024_25@a133f37a": { + # SHA-256 of the staged attempt's file (staged_manifest.json and the Hub's LFS object, 342,348,841 bytes). + "microcosm_uk_2024_25@a6299504": { "logical_name": "microcosm_uk_2024_25", "repo_id": "policyengine/populace-uk-private", "repo_type": "dataset", "path": "microcosm_uk_2024_25.h5", "revision": MICROCOSM_REVISION, + "sha256": "b96a8cd625b645a409c063b1de5158532708a2f0d3c3897a794a8fb0bbd5064e", + "data_package": "microcosm-data", "data_version": "0.1.0", # packages/microcosm-data at 67ee2ddf3 + "built_with": "policyengine-uk 2.151.0 (policyengine-core 3.33.3)", + "build_id": MICROCOSM_REVISION, + "certified": [], # its release certifies it via bundle_certification; producer gates do not. + "bundle_certification": CERTIFIED_BUNDLE, + "release_manifest": MICROCOSM_RELEASE_MANIFEST, + "region_datasets": BINDING_REGION_DATASETS, + "local_area": BINDING_LOCAL_AREA, + # Solved at 2025 and written divided by the model's 2024-to-2025 projection, as a133f37a's are. + "ageing_anchor": {"year": 2025, "calibration_year": 2025, + "source": "uk-frs-calibration-attempt-20261025T091414Z-a6299504: build_record.json " + "uk_national_export_readback weight_year (dataset_year 2024, solve_year 2025, " + "engine_projection_factor 1.0072); calibration_diagnostics.json dates all 1,268 " + "targets 2025"}, + }, + "microcosm_uk_2024_25@a133f37a": { + "logical_name": "microcosm_uk_2024_25", + "repo_id": "policyengine/populace-uk-private", "repo_type": "dataset", + "path": "microcosm_uk_2024_25.h5", "revision": MICROCOSM_REVISION_A133, "sha256": "64f83900ca3a1242fb017585234844ce89f743d32e41cd2b6558057777f914aa", "data_package": "microcosm-data", "data_version": "0.1.0", "built_with": "policyengine-uk 2.147.2 (policyengine-core 3.33.3)", - "build_id": MICROCOSM_REVISION, + "build_id": MICROCOSM_REVISION_A133, "certified": [], # #715 is draft; producer gates are not released bundle certification. - "bundle_certification": CERTIFIED_BUNDLE, - "release_manifest": MICROCOSM_RELEASE_MANIFEST, + "bundle_certification": BUNDLE_715, + "release_manifest": MICROCOSM_RELEASE_MANIFEST_A133, + "region_datasets": FALLBACK_REGION_DATASETS, + "local_area": FALLBACK_LOCAL_AREA, # The build solves its household weights at 2025 and writes them divided by policyengine-uk's own # 2024-to-2025 projection of household_weight, so the model's 2025 weights are the solved ones # (microcosm weight_year.py, microcosm#796 A19). The Enhanced FRS's 2025 weights are instead its calibrated @@ -183,7 +310,7 @@ }, } UNCERTIFIED = ("uncertified: the exact released policyengine.py bundle/model/core and artifact are required; " - "the binding Microcosm UK Budget release from #715 is pending") + "d2011's binding Microcosm UK Budget pair is released in policyengine.py 6.11.1") class DatasetMismatch(RuntimeError): @@ -285,11 +412,64 @@ def model_versions(): "bundle_package": "policyengine", "bundle_version": bundle_version} +def certified_core(manifest): + """The core a UK bundle's certificate covers, by policyengine.py's rule (provenance/manifest.py + DataCertification; provenance/receipt.py _certificate_fields), made stricter where that rule would fall back to + the pin. core_binding "certified" must name the core in certified_for_core_version, and the bundle's core pin + must equal it; "bundle_pin" (or no binding, before October 2026) covers the bundle's core pin, and a named core + must equal it. None for anything else (an unnamed "certified" core, a named core off the pin, an unknown + binding), which certifies nothing.""" + certificate = manifest["data_releases"]["uk"]["certification"] + pin = manifest["packages"]["policyengine-core"]["version"] + binding, named = certificate.get("core_binding"), certificate.get("certified_for_core_version") + if binding == "certified": + return named if named == pin else None + if binding in (None, "bundle_pin"): + return named or pin if named in (None, pin) else None + return None + + +def _local_area_routed(release, local, model_version): + """Whether a release routes areas to exactly the pinned local-area cut, on the pinned basis. Fails closed while + any part of the cut is pending.""" + if any(local[key] is None for key in ("path", "revision", "sha256", "manifest_sha256")): + return False + entries = [entry for entry in release["datasets"].values() + if isinstance(entry, dict) and entry.get("path") == local["path"]] + if (len(entries) != 1 + or any(entries[0].get(key) != local[key] for key in ("repo_id", "revision", "sha256")) + or entries[0].get("repo_type", local["repo_type"]) != local["repo_type"] + or release.get("regional_release_kind") != "local_area" + or release.get("regional_release_manifest_uri") != local["release_manifest_uri"] + or release.get("regional_source_manifest_uri") != local["source_manifest_uri"] + or release.get("regional_source_manifest_sha256", local["manifest_sha256"]) != local["manifest_sha256"]): + return False + regional = release.get("regional_certification") + if local["model_basis"] == "built_with": + # Both halves on the pair's own model: an accepted mismatch is not this pair. + return regional is None and local["built_with_model_version"] == model_version + return (local["model_basis"] == "accepted_model_mismatch" and isinstance(regional, dict) + and release.get("regional_source_manifest_sha256") == local["manifest_sha256"] + and regional.get("compatibility_basis") == "accepted_model_mismatch" + and regional.get("certified_for_model_version") == model_version + and regional.get("built_with_model_version") == local["built_with_model_version"] + and regional.get("regional_manifest_uri") == local["source_manifest_uri"] + and regional.get("regional_manifest_sha256") == local["manifest_sha256"] + and regional.get("area_receipts") == "uncertified") + + def _certified_by_installed_bundle(spec, versions): - """Fail closed unless released-manifest or ledger evidence certifies the exact pair.""" + """Fail closed unless released-manifest or ledger evidence certifies the exact pair. A released-manifest + certificate also needs the pull request and commit that certify it, and a pair with a local-area cut needs that + exact cut.""" certification = spec.get("bundle_certification") + regions = spec.get("region_datasets", {"national": {"path_template": spec["path"]}}) + if not all(isinstance(template, dict) and template.get("path_template") for template in regions.values()): + return False # a pending file (BINDING_LOCAL_AREA_PATH) certifies nothing if (certification is None or not certification.get("bundle_version") or not certification.get("manifest_sha256") and "ledger_source_path" not in certification + or "ledger_source_path" not in certification + and not (certification.get("certification_url") and certification.get("certification_commit")) or any(versions.get(key) != certification[key] for key in ("bundle_package", "bundle_version", "model_version", "core_version"))): return False @@ -313,14 +493,16 @@ def _certified_by_installed_bundle(spec, versions): if (manifest["policyengine_version"] != versions["bundle_version"] or release["policyengine_version"] != versions["bundle_version"] or manifest["packages"]["policyengine-core"]["version"] != versions["core_version"] - or release["certification"]["certified_for_core_version"] != versions["core_version"] + or certified_core(manifest) != versions["core_version"] or release["model_package"]["version"] != versions["model_version"] or release["model_package"]["sha256"] != certification["model_wheel_sha256"] or release["build_id"] != spec["build_id"] or artifact["build_id"] != spec["build_id"] or release["release_manifest_uri"] != spec["release_manifest"]["uri"] or release["source_manifest_uri"] != spec["release_manifest"]["source_uri"] - or release["region_datasets"] != {"national": {"path_template": spec["path"]}}): + or release["region_datasets"] != regions + or "local_area" in spec + and not _local_area_routed(release, spec["local_area"], versions["model_version"])): return False matches = (release["bundle_id"] == certification["bundle_id"] and release["certification"]["certified_for_model_version"] == versions["model_version"] diff --git a/src/triple_lock/engine.py b/src/triple_lock/engine.py index caf3c688..8fda5341 100644 --- a/src/triple_lock/engine.py +++ b/src/triple_lock/engine.py @@ -1,23 +1,23 @@ """Full PolicyEngine UK runs of the two rules on a growth path, each in its own process, cached by input. Every fiscal and household figure in the results comes from a job here; nothing -is scaled from another run. The binding primary is policyengine.py's certified -UK Budget pair proposed by #715: Microcosm national cut -microcosm-uk-2024-25-national-20261023T075039Z-a133f37a, policyengine-uk -2.147.2 and policyengine-core 3.33.3. Publication fails closed until its -policyengine.py carrier is released. Enhanced FRS 1.58.2 is the paired -sensitivity. All datasets have exact revision and SHA-256 pins (datasets.py). +is scaled from another run. The binding primary is the certified UK Budget +pair d2011 chose (microcosm#1396): Microcosm national cut +microcosm-uk-2024-25-national-20261025T091414Z-a6299504, policyengine-uk +2.151.0 and policyengine-core 3.33.3, released in policyengine.py 6.11.1. +Publication fails closed without exactly that installed bundle. Enhanced FRS +1.58.2 is the paired sensitivity. All datasets have exact revision and SHA-256 pins (datasets.py). The provisional pre-Budget release used Enhanced FRS; bridge step (b) measures the move to Microcosm on the same model. Historical receipts remain intact. -This bundle predates #2557 (September-CPI benefit uprating, first in 2.148.2) -and #2576 (earnings-linked guarantee and derived HB/Savings Credit, first in -2.151.2). Benefit rates use the installed lagged-calendar-CPI branch. The -engine still earnings-links the Pension Credit guarantee; HB pension-age -allowances and Savings Credit thresholds retain their installed CPI projection -under d1753's default. The age addition (#2340) is absent. The explicit -additional-State-Pension September-CPI pin takes precedence over #2336's -formula, which is present in 2.147.2. +This model has #2557 (September-CPI benefit uprating, first in 2.148.2) and +predates #2576 (earnings-linked guarantee and derived HB/Savings Credit, first +in 2.151.2). Benefit rates follow the previous September's CPI. The engine +still earnings-links the Pension Credit guarantee; HB pension-age allowances +and Savings Credit thresholds name lagged calendar CPI and retain that +installed projection under d1753's default. The age addition (#2340) is +absent. The explicit additional-State-Pension September-CPI pin takes +precedence over #2336's formula, which is present in 2.151.0. How a path enters the model --------------------------- @@ -26,7 +26,7 @@ amount as CPI), and its statutory inputs for 2026-2038 (September CPI and May-July AWE, each at its observation date) replace the model's ``statutory_uprating_inputs``, in a Scenario applied before the data load. -On 2.147.2, dated reforms passed during construction also rebuild the derived +On 2.147.2 and 2.151.0, dated reforms passed during construction also rebuild the derived series, including the triple lock. The engine uses the before-data Scenario so projected inputs follow the path, then sets explicit flat-rate amounts from its own rules to preserve their precision, horizon and earnings-path @@ -35,8 +35,9 @@ records, and fails unless, in every year 2027-28 to 2039-40: * benefit uprating follows the index the installed model names: on 2.147.2, - the previous calendar year's CPI; on models with #2557, the previous - September CPI (the Pension Credit guarantee follows May-July earnings); + the previous calendar year's CPI; on models with #2557 (2.151.0), the + previous September CPI (the Pension Credit guarantee follows May-July + earnings); * CPI-indexed thresholds (the NI lower earnings limit) grow by the path's calendar CPI the same year; * employment income grows by the path's earnings the same year; @@ -51,9 +52,9 @@ (reported benefits, consumption) and private pension income (the previous year's RPI, capped at 5%). Not moving: dividend, property and savings income, self-employment income, rents and council tax, whose growth ``not_moving`` -records on every run. On 2.147.2, April 2027's benefit uprating uses the -installed 2026 calendar-CPI forecast; subsequent upratings use the path's -previous calendar year. Additional State Pension uses September CPI throughout. +records on every run. On 2.151.0, April 2027's benefit uprating uses the +path's September 2026 CPI input; subsequent upratings use the path's previous +September. Additional State Pension uses September CPI throughout. The State Pension flat rates are set from each rule applied to the path's statutory inputs (to 0.1 point as published, by rules.round_rate), or from a @@ -82,7 +83,7 @@ State Pension age that year. Split by each year's type, as policyengine-uk's own flat-rate parts are, it never pays the band between the two flat rates twice; historical policyengine-uk 2.125.1 scales it by the flat rates' ratio - (#1941), while 2.147.2 uses September CPI from the data year (#2336). The + (#1941), while 2.147.2 and 2.151.0 use September CPI from the data year (#2336). The engine's explicit path input takes precedence over either formula. Every run checks the data year's identity basic + new + additional (+ age addition when defined) = the counted report, person by person (state_pension_accounting). The age @@ -90,7 +91,7 @@ * the Pension Credit standard minimum guarantee rises with the path's May-July earnings growth (config.PENSION_CREDIT_GUARANTEE; SSAA 1992 s150A). Its Savings Credit threshold and pension-age HB allowances retain their installed - CPI projection on 2.147.2 (d1753). They follow the applied guarantee only on + CPI projection on 2.147.2 and 2.151.0 (d1753). They follow the applied guarantee only on models that actually provide #2576's linked builders. A Scenario simulation builds a second, default-path simulation as its @@ -671,7 +672,7 @@ def set_flat_rates(sim, levels, extra=None): The macro Scenario has already processed its parameter tree. A subsequent guarantee override updates Savings Credit or pension-age HB amounts only when the installed model has the corresponding linked projection builder. - UK 2.147.2 has neither: its installed CPI projections stay in force under + UK 2.147.2 and 2.151.0 have neither: their installed CPI projections stay in force under d1753. A maximum parameter alone does not establish either derivation. Explicit dependent amounts in ``extra`` win for projection sensitivities. """ @@ -755,7 +756,7 @@ def pinned_inputs(sim, years, sep_cpi, demography=None, anchor=None, retyped_lev does. So basic + new + additional (+ age addition) equals the payable reported pension in the data year for every person, whatever the type, and a cut in the flat rates moves only the flat-rate parts. Historical policyengine-uk 2.125.1 uprates it by the flat rates' ratio (#1941); - 2.147.2 contains #2336's September-CPI formula. This explicit input still wins + 2.147.2 and 2.151.0 contain #2336's September-CPI formula. This explicit input still wins over that formula, so the selected path controls the additional pension. Splitting it by the survey year's type instead would pay the band between the two flat rates twice for everyone a cohort type moves from basic to new. diff --git a/src/triple_lock/pipeline.py b/src/triple_lock/pipeline.py index 533e6b69..c2866afe 100644 --- a/src/triple_lock/pipeline.py +++ b/src/triple_lock/pipeline.py @@ -8,8 +8,8 @@ redact_records. * ``expected_value``: the calibrated distribution of paths and the stratified sample of full runs (expected_value.py), on the certified Microcosm UK Budget - primary and the paired Enhanced FRS 1.58.2 sensitivity. Publication waits for - the policyengine.py #715 carrier release. + primary and the paired Enhanced FRS 1.58.2 sensitivity. Publication requires + policyengine.py 6.11.1, the release carrying d2011's pair. * ``trajectories``: a few paths we fully understand, past years and the method backtests (trajectories.py). * ``coverage``: what each dataset holds against DWP's tables, for the UK and Great Britain, in 2026-27 to 2030-31, @@ -127,9 +127,9 @@ def method_limitations(): for i, text in enumerate(wording): if text.startswith("In every run April 2027"): wording[i] = ( - "On policyengine-uk 2.147.2, April 2027 benefit uprating uses the installed calendar-2026 " - "CPI forecast; later years use the path's previous calendar-year CPI. This bundle predates " - "#2557's September-CPI benefit uprating. Additional State Pension follows September CPI.") + "On policyengine-uk 2.151.0, benefit uprating follows the previous September's CPI (#2557), to " + "0.1 point and never cut: April 2027 uses the path's September 2026 CPI input and later years the " + "path's September CPI the year before. Additional State Pension follows September CPI.") elif text.startswith("The population is aged statically"): wording[i] = ( "The population is aged statically: each record represents a person of its age in every year. " @@ -145,18 +145,20 @@ def method_limitations(): "Microcosm records are built from survey donors. The same minimum of ten contributing " "records and complementary withholding applies to the primary and Enhanced FRS sensitivity.") elif text.startswith("policyengine-uk pays Scotland"): - wording[i] = "policyengine-uk 2.147.2 has no 25p State Pension age addition at 80 (#2340 is unmerged)." + wording[i] = "policyengine-uk 2.151.0 has no 25p State Pension age addition at 80 (#2340 is unmerged)." elif text.startswith("The primary model/data pair"): wording[i] = ( - "The binding primary is policyengine.py's UK Budget pair proposed in #715: Microcosm national " - "cut microcosm-uk-2024-25-national-20261023T075039Z-a133f37a, policyengine-uk 2.147.2 and " - "policyengine-core 3.33.3. Publication fails closed until its certified policyengine.py release " - "exists. Enhanced FRS 1.58.2 is the paired sensitivity. This model predates September-CPI " - "benefit uprating (#2557) and linked HB/Savings Credit derivation (#2576): those retain the " - "installed CPI projection (d1753); the Pension Credit guarantee still follows May-July earnings.") + "The binding primary is the UK Budget pair d2011 certifies (microcosm#1396): Microcosm national " + "cut microcosm-uk-2024-25-national-20261025T091414Z-a6299504, policyengine-uk 2.151.0 and " + "policyengine-core 3.33.3, certified by its policyengine.py 6.11.1 release (policyengine.py#720); " + "publication fails closed without exactly that installed bundle. Enhanced FRS 1.58.2 is the " + "paired sensitivity. This model has September-CPI benefit uprating (#2557) but predates linked " + "HB/Savings Credit derivation (#2576): the Savings Credit thresholds and pension-age HB " + "allowances retain the installed lagged calendar-CPI projection (d1753); the Pension Credit " + "guarantee still follows May-July earnings.") wording.append( "The pre-Budget release used Enhanced FRS and was described as provisional in Vahid's post. " - "The binding release uses Microcosm; bridge step (b), on the same policyengine-uk 2.147.2 model, " + "The binding release uses Microcosm; bridge step (b), on the same policyengine-uk 2.151.0 model, " "measures that change of data.") return wording @@ -183,26 +185,32 @@ class UncertifiedBundle(RuntimeError): def certified_bundle_preflight(log=print): - """Require #715's exact released UK Budget pair without loading any dataset. - - The candidate head is not a released certificate. Dataset provenance checks - the installed release manifest digest and exact model/core/artifact identity; - materialization separately verifies the artifact's bytes before model jobs. - Historical bridge jobs and Enhanced FRS sensitivity use separate entry points. + """Require d2011's exact released UK Budget pair without loading any dataset. + + The primary must be one of d2011's pairs (datasets.D2011_PAIRS), and the bundle + checked is that pair's own: the 2.151.0 pair's, or after a fallback commit moves + config.PRIMARY_DATASET, a133f37a's. The candidate head is not a released + certificate. Dataset provenance checks the installed release manifest digest + and exact model/core/artifact and local-area identity; materialization + separately verifies the artifact's bytes before model jobs. Historical bridge + jobs and Enhanced FRS sensitivity use separate entry points. """ - certification = datasets.CERTIFIED_BUNDLE + remedy = ("The fiscal rebuild requires the certified released policyengine.py UK Budget bundle carrying " + "d2011's pair (microcosm#1396): UK 2.151.0 / core 3.33.3 / Microcosm a6299504, or its fallback, " + "#715's UK 2.147.2 / Microcosm a133f37a with #699's area routing. While the pair's release identities " + "in datasets.py are pending, this refuses. scripts/fill_release_identities.py pins them from the " + "release (for the 2.151.0 pair, policyengine.py 6.11.1): its version, released manifest digest, " + "certifying pull request and commit, and local-area cut, and the carrier in the lock. Install the " + "re-resolved lock (docs/REBUILD.md). A candidate head and producer gates cannot certify a release.") + if PRIMARY_DATASET not in datasets.D2011_PAIRS: + raise UncertifiedBundle(remedy) + primary = PRIMARY_DATASET + certification = datasets.DATASETS[primary]["bundle_certification"] expected = {"policyengine": certification["bundle_version"], "policyengine-uk": certification["model_version"], "policyengine-core": certification["core_version"]} - primary = "microcosm_uk_2024_25@a133f37a" - remedy = ("The fiscal rebuild requires the certified released policyengine.py UK Budget bundle from #715 " - "(https://github.com/PolicyEngine/policyengine.py/pull/715), UK 2.147.2 / core 3.33.3 / " - "Microcosm a133f37a. #715 is draft and its carrier release is pending. After release, pin its " - "version and released manifest digest in datasets.py, update the carrier dependency version, " - "and install the committed lock. The candidate head and producer gates cannot certify a release.") - if PRIMARY_DATASET != primary: - raise UncertifiedBundle(remedy) - if not expected["policyengine"] or not certification["manifest_sha256"]: + if (not expected["policyengine"] or not certification["manifest_sha256"] + or not certification["certification_url"] or not certification["certification_commit"]): raise UncertifiedBundle(remedy) installed = package_versions() if any(installed.get(name) != version for name, version in expected.items()): @@ -210,7 +218,7 @@ def certified_bundle_preflight(log=print): provenance = datasets.provenance(PRIMARY_DATASET) if provenance.get("dataset") != primary or provenance.get("certified") is not True: raise UncertifiedBundle(remedy + " The installed released manifest does not certify the pinned pair.") - log(f"Preflight: exact policyengine {expected['policyengine']} UK Budget bundle and Microcosm a133f37a certified") + log(f"Preflight: exact policyengine {expected['policyengine']} UK Budget bundle and {primary} certified") return {"certified_bundle": {"passed": True, "packages": expected, "primary": provenance}} diff --git a/tests/fixtures/policyengine-bundle-manifests/pr-555-merge-37d25c42.json b/tests/fixtures/policyengine-bundle-manifests/pr-555-merge-37d25c42.json new file mode 100644 index 00000000..2e7e48a9 --- /dev/null +++ b/tests/fixtures/policyengine-bundle-manifests/pr-555-merge-37d25c42.json @@ -0,0 +1,339 @@ +{ + "bundle_version": "6.2.5", + "citation": { + "publisher": "PolicyEngine", + "title": "PolicyEngine bundle 6.2.5", + "type": "software-bundle", + "version": "6.2.5" + }, + "countries": { + "uk": { + "model_package": "policyengine-uk" + }, + "us": { + "model_package": "policyengine-us" + } + }, + "data_releases": { + "uk": { + "build_id": "policyengine-uk-data-1.58.0", + "bundle_id": "uk-6.2.5", + "certification": { + "built_with_model_git_sha": "4cbedbecca352f52dffe47e849553c2759a00711", + "built_with_model_version": "2.122.2", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.125.1", + "compatibility_basis": "legacy_compatible_model_package", + "data_build_fingerprint": "sha256:c094d7006fdd81df220090284212f16bbffb15624643bc442c4bb3e1b1e5dac8", + "data_build_id": "policyengine-uk-data-1.58.0" + }, + "certified_data_artifact": { + "build_id": "policyengine-uk-data-1.58.0", + "data_package": { + "name": "policyengine-uk-data", + "version": "1.58.0" + }, + "dataset": "enhanced_frs_2024_25", + "sha256": "5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6", + "uri": "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@1.58.0" + }, + "country_id": "uk", + "data_package": { + "name": "policyengine-uk-data", + "release_manifest_path": "releases/1.58.0/release_manifest.json", + "release_manifest_revision": "fdeba38517bd86067f34c0a5d8ed8923efb3b10a", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "version": "1.58.0" + }, + "data_producer": "populace", + "datasets": { + "enhanced_frs_2024_25": { + "path": "enhanced_frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6" + }, + "enhanced_frs_2024_25_tiny": { + "path": "enhanced_frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "a2e938b6c68952dc6fa3af998501371d9014b66263baef262d4f649fe6f19dd5" + }, + "frs_2024_25": { + "path": "frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "2ec0ea6c10bc82d030a071474c1b376780ea88053e1659d784b4253662341634" + }, + "frs_2024_25_tiny": { + "path": "frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "e91d17c4bae92067222272da33918f15027f65a58e3765c7bdff8aa119df914e" + }, + "local_authority_weights": { + "path": "local_authority_weights.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "0f8e43877f13cea86ea7bd78ce938769d0c2746ac0b191be661c25608aa786fc" + }, + "local_geography_weights": { + "path": "local_geography_weights.csv.gz", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "2c4026139eb69d26af5a0ddad7e7fe9ee981b89a0968e77a5f41820091f641c2" + }, + "parliamentary_constituency_weights": { + "path": "parliamentary_constituency_weights.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "da3d021bf6cd57f06e878094e48524a6a691207231158d9a109da73ea459e900" + } + }, + "default_dataset": "enhanced_frs_2024_25", + "default_dataset_uri": "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@1.58.0", + "model_package": { + "name": "policyengine-uk", + "sha256": "a0983b4cef868467578a4fd0d163a977a0da8f710dd5194ec01c75f66d512c5a", + "version": "2.125.1", + "wheel_url": "https://files.pythonhosted.org/packages/ec/3a/1d90bad4346d1a8c2715be211ed3d3d7f48917da0d90405b8257b7304091/policyengine_uk-2.125.1-py3-none-any.whl" + }, + "policyengine_version": "6.2.5", + "region_datasets": { + "national": { + "path_template": "enhanced_frs_2024_25.h5" + } + }, + "release_manifest_uri": "https://huggingface.co/policyengine/policyengine-uk-data-private/resolve/fdeba38517bd86067f34c0a5d8ed8923efb3b10a/releases/1.58.0/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://model/policyengine/policyengine-uk-data-private@fdeba38517bd86067f34c0a5d8ed8923efb3b10a/releases/1.58.0/release_manifest.json", + "version": "policyengine-uk-data-1.58.0" + }, + "us": { + "build_id": "populace-us-2024-spm-20260915", + "bundle_id": "us-6.2.5", + "certification": { + "built_with_model_version": "2.2.1", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.2.1", + "compatibility_basis": "built_with_model_package", + "data_build_id": "populace-us-2024-spm-20260915" + }, + "certified_data_artifact": { + "build_id": "populace-us-2024-spm-20260915", + "data_package": { + "name": "microcosm-data", + "version": "0.1.0" + }, + "dataset": "populace_us_2024", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84", + "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915" + }, + "country_id": "us", + "data_package": { + "name": "microcosm-data", + "release_manifest_path": "releases/populace-us-2024-spm-20260915/release_manifest.json", + "release_manifest_revision": "populace-us-2024-spm-20260915", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "version": "0.1.0" + }, + "data_producer": "populace", + "datasets": { + "build_manifest": { + "path": "releases/populace-us-2024-spm-20260915/build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "f3fd734867324bca7619f5f083a8115e32104bea3948d48723b345cfd39fa03b" + }, + "calibration_diagnostics": { + "path": "releases/populace-us-2024-spm-20260915/calibration_diagnostics.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e" + }, + "parent_build_manifest": { + "path": "releases/populace-us-2024-spm-20260915/parent_build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "1990e8fd37ce66f499b8e6600c5896701bb07015be132a12de64e321a2edc67e" + }, + "parent_release_manifest": { + "path": "releases/populace-us-2024-spm-20260915/parent_release_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506" + }, + "populace_us_2024": { + "path": "populace_us_2024.h5", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84" + }, + "populace_us_2024_acs_local": { + "path": "populace_us_2024_acs_local.h5", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "revision": "populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z", + "sha256": "769756c31f3ca646d12c272511744dec04c0e68870c6946dd945fbba65b6a7ec" + }, + "source_enrichment": { + "path": "releases/populace-us-2024-spm-20260915/source_enrichment.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "85329907b2b9c4299855b0970b1f24ede5819d3b16c312eb7400b4203bd71203" + }, + "source_enrichment_compatibility": { + "path": "releases/populace-us-2024-spm-20260915/source_enrichment_compatibility.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "63109acffd486453c99c476dece6b0154cc5388e5a430830a30744a2249444d1" + }, + "source_spm_independence_provenance": { + "path": "releases/populace-us-2024-spm-20260915/source_spm_independence_provenance.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "265e844178ed3856d68c1638820f3089e0e1e1363aa4677e33bebb37cbd5bff3" + }, + "source_spm_person_independence": { + "path": "releases/populace-us-2024-spm-20260915/source_spm_person_independence.csv", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "22b5968d90fecfeef7614583e493fe10cc16bda8b5be82e6f49a5bc2102d3ce5" + }, + "us_source_coverage": { + "path": "releases/populace-us-2024-spm-20260915/us_source_coverage.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" + } + }, + "default_dataset": "populace_us_2024", + "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915", + "model_package": { + "name": "policyengine-us", + "sha256": "0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6", + "version": "2.2.1", + "wheel_url": "https://files.pythonhosted.org/packages/5e/09/86ac3968d2a18e28fce3ba1186fc21728e9f299b96a806d6f204f1e0aaed/policyengine_us-2.2.1-py3-none-any.whl" + }, + "policyengine_version": "6.2.5", + "region_datasets": { + "congressional_district": { + "path_template": "populace_us_2024_acs_local.h5" + }, + "national": { + "path_template": "populace_us_2024.h5" + }, + "state": { + "path_template": "populace_us_2024_acs_local.h5" + } + }, + "regional_release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z/releases/populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z/release_manifest.json", + "regional_source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z/releases/populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z/release_manifest.json", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "version": "populace-us-2024-spm-20260915" + } + }, + "dataset_overlays": { + "uk": { + "enhanced_frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "584ae33d80ca0431254610a3f8254d132da73477d31966d6446282861ecae50d" + }, + "frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "df26d4d7af9d164aa2d064181b39290292d2f62bb26fee6126fc095fc06da292" + }, + "populace_uk_2023": { + "data_package_name": "populace-data", + "path": "populace_uk_2023.h5", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "revision": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "sha256": "f17306ccb2aad7ff0130be3589b560afb2e2a12a943570911cd0c77f07934833" + } + } + }, + "extras": { + "models": [ + "policyengine-core", + "policyengine-us", + "policyengine-uk", + "spm-calculator" + ], + "uk": [ + "policyengine-core", + "policyengine-uk" + ], + "us": [ + "policyengine-core", + "policyengine-us", + "spm-calculator" + ] + }, + "measurements": { + "spm": { + "as_of": null, + "county_vintage": "2020", + "forecast_content_sha256": "3d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173", + "geography_id": null, + "geography_kind": "county", + "scenario": "ce_trend" + } + }, + "packages": { + "policyengine": { + "import_name": "policyengine", + "install_requirement": "policyengine==6.2.5", + "name": "policyengine", + "role": "bundle_carrier", + "version": "6.2.5" + }, + "policyengine-core": { + "import_name": "policyengine_core", + "install_requirement": "policyengine-core==3.33.0", + "name": "policyengine-core", + "role": "runtime_dependency", + "version": "3.33.0" + }, + "policyengine-uk": { + "country": "uk", + "import_name": "policyengine_uk", + "install_requirement": "policyengine-uk==2.125.1", + "name": "policyengine-uk", + "role": "country_model", + "version": "2.125.1" + }, + "policyengine-us": { + "country": "us", + "import_name": "policyengine_us", + "install_requirement": "policyengine-us==2.2.1", + "name": "policyengine-us", + "role": "country_model", + "version": "2.2.1" + }, + "spm-calculator": { + "country": "us", + "import_name": "spm_calculator", + "install_requirement": "spm-calculator==1.0.0", + "name": "spm-calculator", + "role": "runtime_dependency", + "sha256": "e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305", + "version": "1.0.0", + "wheel_url": "https://files.pythonhosted.org/packages/19/a0/c484f69a0ebf88a9b9fd0ac28176f8df66550f46714f1b0c5ec0b360824e/spm_calculator-1.0.0-py3-none-any.whl" + } + }, + "policyengine_version": "6.2.5", + "schema_version": 2 +} diff --git a/tests/fixtures/policyengine-bundle-manifests/pr-704-merge-13192b52.json b/tests/fixtures/policyengine-bundle-manifests/pr-704-merge-13192b52.json new file mode 100644 index 00000000..e17db3ad --- /dev/null +++ b/tests/fixtures/policyengine-bundle-manifests/pr-704-merge-13192b52.json @@ -0,0 +1,392 @@ +{ + "bundle_version": "6.10.2", + "citation": { + "publisher": "PolicyEngine", + "title": "PolicyEngine bundle 6.10.2", + "type": "software-bundle", + "version": "6.10.2" + }, + "countries": { + "uk": { + "model_package": "policyengine-uk" + }, + "us": { + "model_package": "policyengine-us" + } + }, + "data_releases": { + "uk": { + "build_id": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "bundle_id": "uk-6.10.2", + "certification": { + "built_with_model_version": "2.132.6", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.132.6", + "compatibility_basis": "built_with_model_package", + "core_binding": "bundle_pin", + "data_build_id": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71" + }, + "certified_data_artifact": { + "build_id": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "data_package": { + "name": "microcosm-data", + "version": "0.1.0" + }, + "dataset": "microcosm_uk_2024_25", + "sha256": "810ba980005a8b907cb51464279fa5e0125464b7af9a4871b7f6744c30951ece", + "uri": "hf://policyengine/populace-uk-private/microcosm_uk_2024_25.h5@microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71" + }, + "country_id": "uk", + "data_package": { + "name": "microcosm-data", + "release_manifest_path": "releases/microcosm-uk-2024-25-national/release_manifest.json", + "release_manifest_revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "version": "0.1.0" + }, + "data_producer": "populace", + "datasets": { + "calibration_diagnostics": { + "path": "releases/microcosm-uk-2024-25-national/calibration_diagnostics.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "34978e2bfde3f2641769c9c3a8f60455c59ecae5a50524b359d74388d7edc888" + }, + "microcosm_uk_2024_25": { + "path": "microcosm_uk_2024_25.h5", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "810ba980005a8b907cb51464279fa5e0125464b7af9a4871b7f6744c30951ece" + }, + "microcosm_uk_2024_25_calibration": { + "path": "microcosm_uk_2024_25_calibration.npz", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "104a8253bf1aebc91147c9bbb40e2603d1025317ba8c6b991383d5e4d91852c2" + }, + "microcosm_uk_2024_25_dense": { + "path": "microcosm_uk_2024_25_dense.h5", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "revision": "microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe", + "sha256": "8e10e130d0260e0b52e2e29902f0502ebdff76a14a3e57d2c876bf4cd8db5872" + }, + "release_certification": { + "path": "releases/microcosm-uk-2024-25-national/release_certification.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "6a68cc93595fc5a7ad451d75a902523c25659de27014f50431dd5e19af5bd959" + }, + "release_cut_gates": { + "path": "releases/microcosm-uk-2024-25-national/release_cut_gates.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "c91c1364d9c3be98493039d4e1dd8c2256881a434c1f124dd2ed0701bfaf7751" + }, + "score_vs_enhanced_frs": { + "path": "releases/microcosm-uk-2024-25-national/score_vs_enhanced_frs.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "ba8a675f36e7c836bf5521e5933e21bb689442734b7340e05cabd3e0cc55323d" + }, + "spine_gates": { + "path": "releases/microcosm-uk-2024-25-national/spine_gates.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "6dbba976dd1304c07da531382cecb11c9cb69512de0ee65126b71f032a0f37d6" + }, + "terminal_gates": { + "path": "releases/microcosm-uk-2024-25-national/terminal_gates.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "e29439d5ce0ea098d32579284ac06f26f086feba2fb8d12d1e88ac9a54840808" + } + }, + "default_dataset": "microcosm_uk_2024_25", + "default_dataset_uri": "hf://policyengine/populace-uk-private/microcosm_uk_2024_25.h5@microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "model_package": { + "name": "policyengine-uk", + "sha256": "cc54025f2a3bc54be4f8d024d1e6f11ed29fed75ca513b214f2d2963c5bf4354", + "version": "2.132.6", + "wheel_url": "https://files.pythonhosted.org/packages/11/f2/c003cd7a95cb499737a5b8af0d619ba4e759c8bc7e55a2bb5c7efa46abd1/policyengine_uk-2.132.6-py3-none-any.whl" + }, + "policyengine_version": "6.10.2", + "region_datasets": { + "constituency": { + "path_template": "microcosm_uk_2024_25_dense.h5" + }, + "local_authority": { + "path_template": "microcosm_uk_2024_25_dense.h5" + }, + "national": { + "path_template": "microcosm_uk_2024_25.h5" + } + }, + "regional_release_kind": "local_area", + "regional_release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe/releases/microcosm-uk-2024-25-dense/release_manifest.json", + "regional_source_manifest_uri": "hf://dataset/policyengine/populace-uk-private@microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe/releases/microcosm-uk-2024-25-dense/release_manifest.json", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71/releases/microcosm-uk-2024-25-national/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://dataset/policyengine/populace-uk-private@microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71/releases/microcosm-uk-2024-25-national/release_manifest.json", + "version": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71" + }, + "us": { + "build_id": "populace-us-2024-spm-20260915", + "bundle_id": "us-6.10.2", + "certification": { + "built_with_model_version": "2.2.1", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.2.1", + "compatibility_basis": "built_with_model_package", + "data_build_id": "populace-us-2024-spm-20260915" + }, + "certified_data_artifact": { + "build_id": "populace-us-2024-spm-20260915", + "data_package": { + "name": "microcosm-data", + "version": "0.1.0" + }, + "dataset": "populace_us_2024", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84", + "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915" + }, + "country_id": "us", + "data_package": { + "name": "microcosm-data", + "release_manifest_path": "releases/populace-us-2024-spm-20260915/release_manifest.json", + "release_manifest_revision": "populace-us-2024-spm-20260915", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "version": "0.1.0" + }, + "data_producer": "populace", + "datasets": { + "build_manifest": { + "path": "releases/populace-us-2024-spm-20260915/build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "f3fd734867324bca7619f5f083a8115e32104bea3948d48723b345cfd39fa03b" + }, + "calibration_diagnostics": { + "path": "releases/populace-us-2024-spm-20260915/calibration_diagnostics.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e" + }, + "parent_build_manifest": { + "path": "releases/populace-us-2024-spm-20260915/parent_build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "1990e8fd37ce66f499b8e6600c5896701bb07015be132a12de64e321a2edc67e" + }, + "parent_release_manifest": { + "path": "releases/populace-us-2024-spm-20260915/parent_release_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506" + }, + "populace_us_2024": { + "path": "populace_us_2024.h5", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84" + }, + "populace_us_2024_acs_local": { + "path": "populace_us_2024_acs_local.h5", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "revision": "populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z", + "sha256": "3ce781c440a7f933b1d952ff4e9d9f65f4698c64b94d2fad165af0c662838d6c" + }, + "source_enrichment": { + "path": "releases/populace-us-2024-spm-20260915/source_enrichment.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "85329907b2b9c4299855b0970b1f24ede5819d3b16c312eb7400b4203bd71203" + }, + "source_enrichment_compatibility": { + "path": "releases/populace-us-2024-spm-20260915/source_enrichment_compatibility.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "63109acffd486453c99c476dece6b0154cc5388e5a430830a30744a2249444d1" + }, + "source_spm_independence_provenance": { + "path": "releases/populace-us-2024-spm-20260915/source_spm_independence_provenance.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "265e844178ed3856d68c1638820f3089e0e1e1363aa4677e33bebb37cbd5bff3" + }, + "source_spm_person_independence": { + "path": "releases/populace-us-2024-spm-20260915/source_spm_person_independence.csv", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "22b5968d90fecfeef7614583e493fe10cc16bda8b5be82e6f49a5bc2102d3ce5" + }, + "us_source_coverage": { + "path": "releases/populace-us-2024-spm-20260915/us_source_coverage.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" + } + }, + "default_dataset": "populace_us_2024", + "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915", + "model_package": { + "name": "policyengine-us", + "sha256": "0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6", + "version": "2.2.1", + "wheel_url": "https://files.pythonhosted.org/packages/5e/09/86ac3968d2a18e28fce3ba1186fc21728e9f299b96a806d6f204f1e0aaed/policyengine_us-2.2.1-py3-none-any.whl" + }, + "policyengine_version": "6.10.2", + "region_datasets": { + "congressional_district": { + "path_template": "populace_us_2024_acs_local.h5" + }, + "national": { + "path_template": "populace_us_2024.h5" + }, + "state": { + "path_template": "populace_us_2024_acs_local.h5" + } + }, + "regional_release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/releases/populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/release_manifest.json", + "regional_source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/releases/populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/release_manifest.json", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "version": "populace-us-2024-spm-20260915" + } + }, + "dataset_overlays": { + "uk": { + "enhanced_frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "584ae33d80ca0431254610a3f8254d132da73477d31966d6446282861ecae50d" + }, + "enhanced_frs_2024_25": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "1.58.1", + "sha256": "056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784" + }, + "enhanced_frs_2024_25_tiny": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "1.58.1", + "sha256": "d5d2bde80f1f028fec98391ea31bb5a0cf8449e3867fc3c1bfe23693b9faecf8" + }, + "frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "df26d4d7af9d164aa2d064181b39290292d2f62bb26fee6126fc095fc06da292" + }, + "frs_2024_25": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "1.58.1", + "sha256": "97af00b2cd2739957f40dcc4de8e0383a6a91d47952c90d4b4921a6007bd2021" + }, + "frs_2024_25_tiny": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "1.58.1", + "sha256": "e4d7662cd3f053620224b7ceff17266783f09407002a72633b043b844cec3f5a" + }, + "populace_uk_2023": { + "data_package_name": "populace-data", + "path": "populace_uk_2023.h5", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "revision": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "sha256": "f17306ccb2aad7ff0130be3589b560afb2e2a12a943570911cd0c77f07934833" + } + } + }, + "extras": { + "models": [ + "policyengine-core", + "policyengine-us", + "policyengine-uk", + "spm-calculator" + ], + "uk": [ + "policyengine-core", + "policyengine-uk" + ], + "us": [ + "policyengine-core", + "policyengine-us", + "spm-calculator" + ] + }, + "measurements": { + "spm": { + "as_of": null, + "county_vintage": "2020", + "forecast_content_sha256": "3d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173", + "geography_id": null, + "geography_kind": "county", + "scenario": "ce_trend" + } + }, + "packages": { + "policyengine": { + "import_name": "policyengine", + "install_requirement": "policyengine==6.10.2", + "name": "policyengine", + "role": "bundle_carrier", + "version": "6.10.2" + }, + "policyengine-core": { + "import_name": "policyengine_core", + "install_requirement": "policyengine-core==3.33.3", + "name": "policyengine-core", + "role": "runtime_dependency", + "version": "3.33.3" + }, + "policyengine-uk": { + "country": "uk", + "import_name": "policyengine_uk", + "install_requirement": "policyengine-uk==2.132.6", + "name": "policyengine-uk", + "role": "country_model", + "version": "2.132.6" + }, + "policyengine-us": { + "country": "us", + "import_name": "policyengine_us", + "install_requirement": "policyengine-us==2.2.1", + "name": "policyengine-us", + "role": "country_model", + "version": "2.2.1" + }, + "spm-calculator": { + "country": "us", + "import_name": "spm_calculator", + "install_requirement": "spm-calculator==1.0.0", + "name": "spm-calculator", + "role": "runtime_dependency", + "sha256": "e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305", + "version": "1.0.0", + "wheel_url": "https://files.pythonhosted.org/packages/19/a0/c484f69a0ebf88a9b9fd0ac28176f8df66550f46714f1b0c5ec0b360824e/spm_calculator-1.0.0-py3-none-any.whl" + } + }, + "policyengine_version": "6.10.2", + "schema_version": 2 +} diff --git a/tests/fixtures/policyengine-bundle-manifests/release-6.11.0.json b/tests/fixtures/policyengine-bundle-manifests/release-6.11.0.json new file mode 100644 index 00000000..24783f55 --- /dev/null +++ b/tests/fixtures/policyengine-bundle-manifests/release-6.11.0.json @@ -0,0 +1,392 @@ +{ + "bundle_version": "6.11.0", + "citation": { + "publisher": "PolicyEngine", + "title": "PolicyEngine bundle 6.11.0", + "type": "software-bundle", + "version": "6.11.0" + }, + "countries": { + "uk": { + "model_package": "policyengine-uk" + }, + "us": { + "model_package": "policyengine-us" + } + }, + "data_releases": { + "uk": { + "build_id": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "bundle_id": "uk-6.11.0", + "certification": { + "built_with_model_version": "2.132.6", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.132.6", + "compatibility_basis": "built_with_model_package", + "core_binding": "bundle_pin", + "data_build_id": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71" + }, + "certified_data_artifact": { + "build_id": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "data_package": { + "name": "microcosm-data", + "version": "0.1.0" + }, + "dataset": "microcosm_uk_2024_25", + "sha256": "810ba980005a8b907cb51464279fa5e0125464b7af9a4871b7f6744c30951ece", + "uri": "hf://policyengine/populace-uk-private/microcosm_uk_2024_25.h5@microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71" + }, + "country_id": "uk", + "data_package": { + "name": "microcosm-data", + "release_manifest_path": "releases/microcosm-uk-2024-25-national/release_manifest.json", + "release_manifest_revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "version": "0.1.0" + }, + "data_producer": "populace", + "datasets": { + "calibration_diagnostics": { + "path": "releases/microcosm-uk-2024-25-national/calibration_diagnostics.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "34978e2bfde3f2641769c9c3a8f60455c59ecae5a50524b359d74388d7edc888" + }, + "microcosm_uk_2024_25": { + "path": "microcosm_uk_2024_25.h5", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "810ba980005a8b907cb51464279fa5e0125464b7af9a4871b7f6744c30951ece" + }, + "microcosm_uk_2024_25_calibration": { + "path": "microcosm_uk_2024_25_calibration.npz", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "104a8253bf1aebc91147c9bbb40e2603d1025317ba8c6b991383d5e4d91852c2" + }, + "microcosm_uk_2024_25_dense": { + "path": "microcosm_uk_2024_25_dense.h5", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "revision": "microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe", + "sha256": "8e10e130d0260e0b52e2e29902f0502ebdff76a14a3e57d2c876bf4cd8db5872" + }, + "release_certification": { + "path": "releases/microcosm-uk-2024-25-national/release_certification.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "6a68cc93595fc5a7ad451d75a902523c25659de27014f50431dd5e19af5bd959" + }, + "release_cut_gates": { + "path": "releases/microcosm-uk-2024-25-national/release_cut_gates.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "c91c1364d9c3be98493039d4e1dd8c2256881a434c1f124dd2ed0701bfaf7751" + }, + "score_vs_enhanced_frs": { + "path": "releases/microcosm-uk-2024-25-national/score_vs_enhanced_frs.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "ba8a675f36e7c836bf5521e5933e21bb689442734b7340e05cabd3e0cc55323d" + }, + "spine_gates": { + "path": "releases/microcosm-uk-2024-25-national/spine_gates.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "6dbba976dd1304c07da531382cecb11c9cb69512de0ee65126b71f032a0f37d6" + }, + "terminal_gates": { + "path": "releases/microcosm-uk-2024-25-national/terminal_gates.json", + "repo_id": "policyengine/populace-uk-private", + "revision": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "sha256": "e29439d5ce0ea098d32579284ac06f26f086feba2fb8d12d1e88ac9a54840808" + } + }, + "default_dataset": "microcosm_uk_2024_25", + "default_dataset_uri": "hf://policyengine/populace-uk-private/microcosm_uk_2024_25.h5@microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71", + "model_package": { + "name": "policyengine-uk", + "sha256": "cc54025f2a3bc54be4f8d024d1e6f11ed29fed75ca513b214f2d2963c5bf4354", + "version": "2.132.6", + "wheel_url": "https://files.pythonhosted.org/packages/11/f2/c003cd7a95cb499737a5b8af0d619ba4e759c8bc7e55a2bb5c7efa46abd1/policyengine_uk-2.132.6-py3-none-any.whl" + }, + "policyengine_version": "6.11.0", + "region_datasets": { + "constituency": { + "path_template": "microcosm_uk_2024_25_dense.h5" + }, + "local_authority": { + "path_template": "microcosm_uk_2024_25_dense.h5" + }, + "national": { + "path_template": "microcosm_uk_2024_25.h5" + } + }, + "regional_release_kind": "local_area", + "regional_release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe/releases/microcosm-uk-2024-25-dense/release_manifest.json", + "regional_source_manifest_uri": "hf://dataset/policyengine/populace-uk-private@microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe/releases/microcosm-uk-2024-25-dense/release_manifest.json", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71/releases/microcosm-uk-2024-25-national/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://dataset/policyengine/populace-uk-private@microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71/releases/microcosm-uk-2024-25-national/release_manifest.json", + "version": "microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71" + }, + "us": { + "build_id": "populace-us-2024-spm-20260915", + "bundle_id": "us-6.11.0", + "certification": { + "built_with_model_version": "2.2.1", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.2.1", + "compatibility_basis": "built_with_model_package", + "data_build_id": "populace-us-2024-spm-20260915" + }, + "certified_data_artifact": { + "build_id": "populace-us-2024-spm-20260915", + "data_package": { + "name": "microcosm-data", + "version": "0.1.0" + }, + "dataset": "populace_us_2024", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84", + "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915" + }, + "country_id": "us", + "data_package": { + "name": "microcosm-data", + "release_manifest_path": "releases/populace-us-2024-spm-20260915/release_manifest.json", + "release_manifest_revision": "populace-us-2024-spm-20260915", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "version": "0.1.0" + }, + "data_producer": "populace", + "datasets": { + "build_manifest": { + "path": "releases/populace-us-2024-spm-20260915/build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "f3fd734867324bca7619f5f083a8115e32104bea3948d48723b345cfd39fa03b" + }, + "calibration_diagnostics": { + "path": "releases/populace-us-2024-spm-20260915/calibration_diagnostics.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e" + }, + "parent_build_manifest": { + "path": "releases/populace-us-2024-spm-20260915/parent_build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "1990e8fd37ce66f499b8e6600c5896701bb07015be132a12de64e321a2edc67e" + }, + "parent_release_manifest": { + "path": "releases/populace-us-2024-spm-20260915/parent_release_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506" + }, + "populace_us_2024": { + "path": "populace_us_2024.h5", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84" + }, + "populace_us_2024_acs_local": { + "path": "populace_us_2024_acs_local.h5", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "revision": "populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z", + "sha256": "3ce781c440a7f933b1d952ff4e9d9f65f4698c64b94d2fad165af0c662838d6c" + }, + "source_enrichment": { + "path": "releases/populace-us-2024-spm-20260915/source_enrichment.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "85329907b2b9c4299855b0970b1f24ede5819d3b16c312eb7400b4203bd71203" + }, + "source_enrichment_compatibility": { + "path": "releases/populace-us-2024-spm-20260915/source_enrichment_compatibility.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "63109acffd486453c99c476dece6b0154cc5388e5a430830a30744a2249444d1" + }, + "source_spm_independence_provenance": { + "path": "releases/populace-us-2024-spm-20260915/source_spm_independence_provenance.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "265e844178ed3856d68c1638820f3089e0e1e1363aa4677e33bebb37cbd5bff3" + }, + "source_spm_person_independence": { + "path": "releases/populace-us-2024-spm-20260915/source_spm_person_independence.csv", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "22b5968d90fecfeef7614583e493fe10cc16bda8b5be82e6f49a5bc2102d3ce5" + }, + "us_source_coverage": { + "path": "releases/populace-us-2024-spm-20260915/us_source_coverage.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" + } + }, + "default_dataset": "populace_us_2024", + "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915", + "model_package": { + "name": "policyengine-us", + "sha256": "0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6", + "version": "2.2.1", + "wheel_url": "https://files.pythonhosted.org/packages/5e/09/86ac3968d2a18e28fce3ba1186fc21728e9f299b96a806d6f204f1e0aaed/policyengine_us-2.2.1-py3-none-any.whl" + }, + "policyengine_version": "6.11.0", + "region_datasets": { + "congressional_district": { + "path_template": "populace_us_2024_acs_local.h5" + }, + "national": { + "path_template": "populace_us_2024.h5" + }, + "state": { + "path_template": "populace_us_2024_acs_local.h5" + } + }, + "regional_release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/releases/populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/release_manifest.json", + "regional_source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/releases/populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/release_manifest.json", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "version": "populace-us-2024-spm-20260915" + } + }, + "dataset_overlays": { + "uk": { + "enhanced_frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "584ae33d80ca0431254610a3f8254d132da73477d31966d6446282861ecae50d" + }, + "enhanced_frs_2024_25": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "1.58.1", + "sha256": "056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784" + }, + "enhanced_frs_2024_25_tiny": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "1.58.1", + "sha256": "d5d2bde80f1f028fec98391ea31bb5a0cf8449e3867fc3c1bfe23693b9faecf8" + }, + "frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "df26d4d7af9d164aa2d064181b39290292d2f62bb26fee6126fc095fc06da292" + }, + "frs_2024_25": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "1.58.1", + "sha256": "97af00b2cd2739957f40dcc4de8e0383a6a91d47952c90d4b4921a6007bd2021" + }, + "frs_2024_25_tiny": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "1.58.1", + "sha256": "e4d7662cd3f053620224b7ceff17266783f09407002a72633b043b844cec3f5a" + }, + "populace_uk_2023": { + "data_package_name": "populace-data", + "path": "populace_uk_2023.h5", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "revision": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "sha256": "f17306ccb2aad7ff0130be3589b560afb2e2a12a943570911cd0c77f07934833" + } + } + }, + "extras": { + "models": [ + "policyengine-core", + "policyengine-us", + "policyengine-uk", + "spm-calculator" + ], + "uk": [ + "policyengine-core", + "policyengine-uk" + ], + "us": [ + "policyengine-core", + "policyengine-us", + "spm-calculator" + ] + }, + "measurements": { + "spm": { + "as_of": null, + "county_vintage": "2020", + "forecast_content_sha256": "3d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173", + "geography_id": null, + "geography_kind": "county", + "scenario": "ce_trend" + } + }, + "packages": { + "policyengine": { + "import_name": "policyengine", + "install_requirement": "policyengine==6.11.0", + "name": "policyengine", + "role": "bundle_carrier", + "version": "6.11.0" + }, + "policyengine-core": { + "import_name": "policyengine_core", + "install_requirement": "policyengine-core==3.33.3", + "name": "policyengine-core", + "role": "runtime_dependency", + "version": "3.33.3" + }, + "policyengine-uk": { + "country": "uk", + "import_name": "policyengine_uk", + "install_requirement": "policyengine-uk==2.132.6", + "name": "policyengine-uk", + "role": "country_model", + "version": "2.132.6" + }, + "policyengine-us": { + "country": "us", + "import_name": "policyengine_us", + "install_requirement": "policyengine-us==2.2.1", + "name": "policyengine-us", + "role": "country_model", + "version": "2.2.1" + }, + "spm-calculator": { + "country": "us", + "import_name": "spm_calculator", + "install_requirement": "spm-calculator==1.0.0", + "name": "spm-calculator", + "role": "runtime_dependency", + "sha256": "e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305", + "version": "1.0.0", + "wheel_url": "https://files.pythonhosted.org/packages/19/a0/c484f69a0ebf88a9b9fd0ac28176f8df66550f46714f1b0c5ec0b360824e/spm_calculator-1.0.0-py3-none-any.whl" + } + }, + "policyengine_version": "6.11.0", + "schema_version": 2 +} diff --git a/tests/fixtures/policyengine-bundle-manifests/release-6.2.6.json b/tests/fixtures/policyengine-bundle-manifests/release-6.2.6.json new file mode 100644 index 00000000..602e5ef1 --- /dev/null +++ b/tests/fixtures/policyengine-bundle-manifests/release-6.2.6.json @@ -0,0 +1,339 @@ +{ + "bundle_version": "6.2.6", + "citation": { + "publisher": "PolicyEngine", + "title": "PolicyEngine bundle 6.2.6", + "type": "software-bundle", + "version": "6.2.6" + }, + "countries": { + "uk": { + "model_package": "policyengine-uk" + }, + "us": { + "model_package": "policyengine-us" + } + }, + "data_releases": { + "uk": { + "build_id": "policyengine-uk-data-1.58.0", + "bundle_id": "uk-6.2.6", + "certification": { + "built_with_model_git_sha": "4cbedbecca352f52dffe47e849553c2759a00711", + "built_with_model_version": "2.122.2", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.125.1", + "compatibility_basis": "legacy_compatible_model_package", + "data_build_fingerprint": "sha256:c094d7006fdd81df220090284212f16bbffb15624643bc442c4bb3e1b1e5dac8", + "data_build_id": "policyengine-uk-data-1.58.0" + }, + "certified_data_artifact": { + "build_id": "policyengine-uk-data-1.58.0", + "data_package": { + "name": "policyengine-uk-data", + "version": "1.58.0" + }, + "dataset": "enhanced_frs_2024_25", + "sha256": "5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6", + "uri": "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@1.58.0" + }, + "country_id": "uk", + "data_package": { + "name": "policyengine-uk-data", + "release_manifest_path": "releases/1.58.0/release_manifest.json", + "release_manifest_revision": "fdeba38517bd86067f34c0a5d8ed8923efb3b10a", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "version": "1.58.0" + }, + "data_producer": "populace", + "datasets": { + "enhanced_frs_2024_25": { + "path": "enhanced_frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6" + }, + "enhanced_frs_2024_25_tiny": { + "path": "enhanced_frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "a2e938b6c68952dc6fa3af998501371d9014b66263baef262d4f649fe6f19dd5" + }, + "frs_2024_25": { + "path": "frs_2024_25.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "2ec0ea6c10bc82d030a071474c1b376780ea88053e1659d784b4253662341634" + }, + "frs_2024_25_tiny": { + "path": "frs_2024_25_tiny.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "e91d17c4bae92067222272da33918f15027f65a58e3765c7bdff8aa119df914e" + }, + "local_authority_weights": { + "path": "local_authority_weights.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "0f8e43877f13cea86ea7bd78ce938769d0c2746ac0b191be661c25608aa786fc" + }, + "local_geography_weights": { + "path": "local_geography_weights.csv.gz", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "2c4026139eb69d26af5a0ddad7e7fe9ee981b89a0968e77a5f41820091f641c2" + }, + "parliamentary_constituency_weights": { + "path": "parliamentary_constituency_weights.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.58.0", + "sha256": "da3d021bf6cd57f06e878094e48524a6a691207231158d9a109da73ea459e900" + } + }, + "default_dataset": "enhanced_frs_2024_25", + "default_dataset_uri": "hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@1.58.0", + "model_package": { + "name": "policyengine-uk", + "sha256": "a0983b4cef868467578a4fd0d163a977a0da8f710dd5194ec01c75f66d512c5a", + "version": "2.125.1", + "wheel_url": "https://files.pythonhosted.org/packages/ec/3a/1d90bad4346d1a8c2715be211ed3d3d7f48917da0d90405b8257b7304091/policyengine_uk-2.125.1-py3-none-any.whl" + }, + "policyengine_version": "6.2.6", + "region_datasets": { + "national": { + "path_template": "enhanced_frs_2024_25.h5" + } + }, + "release_manifest_uri": "https://huggingface.co/policyengine/policyengine-uk-data-private/resolve/fdeba38517bd86067f34c0a5d8ed8923efb3b10a/releases/1.58.0/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://model/policyengine/policyengine-uk-data-private@fdeba38517bd86067f34c0a5d8ed8923efb3b10a/releases/1.58.0/release_manifest.json", + "version": "policyengine-uk-data-1.58.0" + }, + "us": { + "build_id": "populace-us-2024-spm-20260915", + "bundle_id": "us-6.2.6", + "certification": { + "built_with_model_version": "2.2.1", + "certified_by": "policyengine.py bundle certification", + "certified_for_model_version": "2.2.1", + "compatibility_basis": "built_with_model_package", + "data_build_id": "populace-us-2024-spm-20260915" + }, + "certified_data_artifact": { + "build_id": "populace-us-2024-spm-20260915", + "data_package": { + "name": "microcosm-data", + "version": "0.1.0" + }, + "dataset": "populace_us_2024", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84", + "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915" + }, + "country_id": "us", + "data_package": { + "name": "microcosm-data", + "release_manifest_path": "releases/populace-us-2024-spm-20260915/release_manifest.json", + "release_manifest_revision": "populace-us-2024-spm-20260915", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "version": "0.1.0" + }, + "data_producer": "populace", + "datasets": { + "build_manifest": { + "path": "releases/populace-us-2024-spm-20260915/build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "f3fd734867324bca7619f5f083a8115e32104bea3948d48723b345cfd39fa03b" + }, + "calibration_diagnostics": { + "path": "releases/populace-us-2024-spm-20260915/calibration_diagnostics.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "870449b44e86b13b25bcea1a57f0e7af37f4d4db18be815eea3acdf9fe6eb40e" + }, + "parent_build_manifest": { + "path": "releases/populace-us-2024-spm-20260915/parent_build_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "1990e8fd37ce66f499b8e6600c5896701bb07015be132a12de64e321a2edc67e" + }, + "parent_release_manifest": { + "path": "releases/populace-us-2024-spm-20260915/parent_release_manifest.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "dd949ba3c4c7a56aff8442c6db5a031d2c84c3da59f5d14832f468c358604506" + }, + "populace_us_2024": { + "path": "populace_us_2024.h5", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84" + }, + "populace_us_2024_acs_local": { + "path": "populace_us_2024_acs_local.h5", + "repo_id": "policyengine/populace-us", + "repo_type": "dataset", + "revision": "populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z", + "sha256": "769756c31f3ca646d12c272511744dec04c0e68870c6946dd945fbba65b6a7ec" + }, + "source_enrichment": { + "path": "releases/populace-us-2024-spm-20260915/source_enrichment.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "85329907b2b9c4299855b0970b1f24ede5819d3b16c312eb7400b4203bd71203" + }, + "source_enrichment_compatibility": { + "path": "releases/populace-us-2024-spm-20260915/source_enrichment_compatibility.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "63109acffd486453c99c476dece6b0154cc5388e5a430830a30744a2249444d1" + }, + "source_spm_independence_provenance": { + "path": "releases/populace-us-2024-spm-20260915/source_spm_independence_provenance.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "265e844178ed3856d68c1638820f3089e0e1e1363aa4677e33bebb37cbd5bff3" + }, + "source_spm_person_independence": { + "path": "releases/populace-us-2024-spm-20260915/source_spm_person_independence.csv", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "22b5968d90fecfeef7614583e493fe10cc16bda8b5be82e6f49a5bc2102d3ce5" + }, + "us_source_coverage": { + "path": "releases/populace-us-2024-spm-20260915/us_source_coverage.json", + "repo_id": "policyengine/populace-us", + "revision": "populace-us-2024-spm-20260915", + "sha256": "6406c8686c292015a5bc7265a42402f89935f9395a8b63510efdd4d9562e7ff5" + } + }, + "default_dataset": "populace_us_2024", + "default_dataset_uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915", + "model_package": { + "name": "policyengine-us", + "sha256": "0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6", + "version": "2.2.1", + "wheel_url": "https://files.pythonhosted.org/packages/5e/09/86ac3968d2a18e28fce3ba1186fc21728e9f299b96a806d6f204f1e0aaed/policyengine_us-2.2.1-py3-none-any.whl" + }, + "policyengine_version": "6.2.6", + "region_datasets": { + "congressional_district": { + "path_template": "populace_us_2024_acs_local.h5" + }, + "national": { + "path_template": "populace_us_2024.h5" + }, + "state": { + "path_template": "populace_us_2024_acs_local.h5" + } + }, + "regional_release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z/releases/populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z/release_manifest.json", + "regional_source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z/releases/populace-us-2024-buildo-acs-local-767312d60-20260923T074941Z/release_manifest.json", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-us/resolve/populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "schema_version": 1, + "source_manifest_uri": "hf://dataset/policyengine/populace-us@populace-us-2024-spm-20260915/releases/populace-us-2024-spm-20260915/release_manifest.json", + "version": "populace-us-2024-spm-20260915" + } + }, + "dataset_overlays": { + "uk": { + "enhanced_frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "enhanced_frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "584ae33d80ca0431254610a3f8254d132da73477d31966d6446282861ecae50d" + }, + "frs_2023_24": { + "data_package_name": "policyengine-uk-data", + "path": "frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "repo_type": "model", + "revision": "655dd07e4bb9c777b00dac044949611f1feb824f", + "sha256": "df26d4d7af9d164aa2d064181b39290292d2f62bb26fee6126fc095fc06da292" + }, + "populace_uk_2023": { + "data_package_name": "populace-data", + "path": "populace_uk_2023.h5", + "repo_id": "policyengine/populace-uk-private", + "repo_type": "dataset", + "revision": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "sha256": "f17306ccb2aad7ff0130be3589b560afb2e2a12a943570911cd0c77f07934833" + } + } + }, + "extras": { + "models": [ + "policyengine-core", + "policyengine-us", + "policyengine-uk", + "spm-calculator" + ], + "uk": [ + "policyengine-core", + "policyengine-uk" + ], + "us": [ + "policyengine-core", + "policyengine-us", + "spm-calculator" + ] + }, + "measurements": { + "spm": { + "as_of": null, + "county_vintage": "2020", + "forecast_content_sha256": "3d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173", + "geography_id": null, + "geography_kind": "county", + "scenario": "ce_trend" + } + }, + "packages": { + "policyengine": { + "import_name": "policyengine", + "install_requirement": "policyengine==6.2.6", + "name": "policyengine", + "role": "bundle_carrier", + "version": "6.2.6" + }, + "policyengine-core": { + "import_name": "policyengine_core", + "install_requirement": "policyengine-core==3.33.0", + "name": "policyengine-core", + "role": "runtime_dependency", + "version": "3.33.0" + }, + "policyengine-uk": { + "country": "uk", + "import_name": "policyengine_uk", + "install_requirement": "policyengine-uk==2.125.1", + "name": "policyengine-uk", + "role": "country_model", + "version": "2.125.1" + }, + "policyengine-us": { + "country": "us", + "import_name": "policyengine_us", + "install_requirement": "policyengine-us==2.2.1", + "name": "policyengine-us", + "role": "country_model", + "version": "2.2.1" + }, + "spm-calculator": { + "country": "us", + "import_name": "spm_calculator", + "install_requirement": "spm-calculator==1.0.0", + "name": "spm-calculator", + "role": "runtime_dependency", + "sha256": "e354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305", + "version": "1.0.0", + "wheel_url": "https://files.pythonhosted.org/packages/19/a0/c484f69a0ebf88a9b9fd0ac28176f8df66550f46714f1b0c5ec0b360824e/spm_calculator-1.0.0-py3-none-any.whl" + } + }, + "policyengine_version": "6.2.6", + "schema_version": 2 +} diff --git a/tests/released_pair.py b/tests/released_pair.py new file mode 100644 index 00000000..85a0b6da --- /dev/null +++ b/tests/released_pair.py @@ -0,0 +1,81 @@ +"""A released UK bundle manifest carrying one of d2011's pairs, as its policyengine.py release would carry it.""" + +from copy import deepcopy +import hashlib +import json + +from triple_lock import datasets +from triple_lock.config import REPO + +VERSION = "99.0.0" # Fictional future release, never a production pin. +LOCAL_AREA_KEYS = ("path", "revision", "sha256", "manifest_sha256") +# A fictional 2.151.0 local-area cut, standing in for the binding pair's own while it is pending. +INVENTED_CUT = datasets.local_area_cut("invented_local_area.h5", "invented-local-area-cut", "c" * 64, "d" * 64, + "2.151.0", "built_with") + + +def cut(name): + """The pair's local-area cut: its pinned one, or the invented one while the binding cut is pending.""" + local = datasets.DATASETS[name]["local_area"] + return deepcopy(local if all(local[key] is not None for key in LOCAL_AREA_KEYS) else INVENTED_CUT) + + +def regional_manifest(local, core="3.33.3"): + """The fields of a local-area cut's own release manifest that the fill checks, as its build writes them. + Synthetic: the real ones are metadata of a private repository.""" + return {"build": {"cut_tag": local["revision"], + "built_with_model_package": {"name": "policyengine-uk", + "version": local["built_with_model_version"]}, + "built_with_core_package": {"name": "policyengine-core", "version": core}}, + "dataset_role": "non_default_local_area", + "artifacts": {"microcosm_uk_2024_25_dense": { + "kind": "microdata", "path": local["path"], "repo_id": local["repo_id"], + "revision": local["revision"], "sha256": local["sha256"]}}} + + +def regional_certification(local, model_version): + """policyengine.py #699's record of an accepted model mismatch between a pair and its local-area cut.""" + return {"compatibility_basis": "accepted_model_mismatch", "certified_for_model_version": model_version, + "built_with_model_version": local["built_with_model_version"], + "accepted_reason": "Synthetic fixture.", "area_receipts": "uncertified", + "regional_manifest_uri": local["source_manifest_uri"], "regional_manifest_sha256": local["manifest_sha256"], + "defaulted_inputs": [], "dropped_columns": [], "limitations": [], "evidence": []} + + +def released(name, local=None, version=VERSION): + """(manifest, local, regional_raw): #715's candidate skeleton re-pointed at the pair ``name``'s pins, routing + areas to the cut ``local`` (default: ``cut(name)``), whose manifest SHA-256 becomes the synthetic bytes'.""" + spec = datasets.DATASETS[name] + certification = spec["bundle_certification"] + local = deepcopy(local or cut(name)) + regional_raw = json.dumps(regional_manifest(local, certification["core_version"])).encode() + local["manifest_sha256"] = hashlib.sha256(regional_raw).hexdigest() + manifest = json.loads((REPO / "data/certification/d1983-candidate-bundle-manifest.json").read_text()) + release = manifest["data_releases"]["uk"] + # Every field the release workflow rewrites for the carrier, as it writes them. + manifest["bundle_version"] = manifest["policyengine_version"] = version + manifest["packages"]["policyengine"].update(version=version, install_requirement=f"policyengine=={version}") + manifest["citation"].update(title=f"PolicyEngine bundle {version}", version=version) + for country, data_release in manifest["data_releases"].items(): + data_release.update(policyengine_version=version, bundle_id=f"{country}-{version}") + core = certification["core_version"] + manifest["packages"]["policyengine-core"].update(version=core, install_requirement=f"policyengine-core=={core}") + release["certification"]["certified_for_core_version"] = certification["core_version"] + release["model_package"].update(version=certification["model_version"], sha256=certification["model_wheel_sha256"]) + release["certification"]["certified_for_model_version"] = certification["model_version"] + release["build_id"] = spec["build_id"] + release["certified_data_artifact"].update( + build_id=spec["build_id"], dataset=spec["logical_name"], sha256=spec["sha256"], + uri=f"hf://{spec['repo_id']}/{spec['path']}@{spec['revision']}", + data_package={"name": spec["data_package"], "version": spec["data_version"]}) + release["release_manifest_uri"] = spec["release_manifest"]["uri"] + release["source_manifest_uri"] = spec["release_manifest"]["source_uri"] + release["region_datasets"] = datasets.area_routed(local["path"]) + release["datasets"]["microcosm_uk_2024_25_dense"] = {key: local[key] for key in ("path", "repo_id", "revision", + "sha256")} + release.update(regional_release_kind="local_area", regional_release_manifest_uri=local["release_manifest_uri"], + regional_source_manifest_uri=local["source_manifest_uri"]) + if local["model_basis"] == "accepted_model_mismatch": + release["regional_source_manifest_sha256"] = local["manifest_sha256"] + release["regional_certification"] = regional_certification(local, certification["model_version"]) + return manifest, local, regional_raw diff --git a/tests/test_binding_bridge.py b/tests/test_binding_bridge.py index 5139db6c..ef76ea3e 100644 --- a/tests/test_binding_bridge.py +++ b/tests/test_binding_bridge.py @@ -19,7 +19,7 @@ def ready_configuration(): config = json.loads(PENDING.read_text()) config["status"] = "ready" - new_packages = {"policyengine": "9.0.0", "policyengine-uk": "2.147.2", "policyengine-core": "3.33.3"} + new_packages = {"policyengine": "9.0.0", "policyengine-uk": "2.151.0", "policyengine-core": "3.33.3"} config["steps"]["model"]["to"]["packages"] = dict(new_packages) config["steps"]["data"]["from"]["packages"] = dict(new_packages) config["steps"]["data"]["to"]["packages"] = dict(new_packages) @@ -51,20 +51,25 @@ def fake_coverage(value): def test_pending_bundle_cannot_be_run(): - with pytest.raises(ValueError, match="bundle is pending:.*#715"): + with pytest.raises(ValueError, match="bundle is pending:.*d2011"): binding_bridge.validate_configuration(json.loads(PENDING.read_text())) -def test_pending_bridge_pins_d1983_model_and_cross_family_data_steps(): +def test_pending_bridge_pins_d2011_model_and_cross_family_data_steps(): configuration = json.loads(PENDING.read_text()) model, data = (configuration["steps"][name] for name in ("model", "data")) assert model["from"]["dataset"] == model["to"]["dataset"] == data["from"]["dataset"] assert model["to"] == data["from"] assert data["to"]["dataset"] == { - "name": "microcosm_uk_2024_25@a133f37a", - "sha256": "64f83900ca3a1242fb017585234844ce89f743d32e41cd2b6558057777f914aa"} + "name": "microcosm_uk_2024_25@a6299504", + "sha256": "b96a8cd625b645a409c063b1de5158532708a2f0d3c3897a794a8fb0bbd5064e"} assert data["from"]["packages"] == data["to"]["packages"] == { - "policyengine": None, "policyengine-uk": "2.147.2", "policyengine-core": "3.33.3"} + "policyengine": None, "policyengine-uk": "2.151.0", "policyengine-core": "3.33.3"} + # Each pin is the registry's: the bridge measures exactly the binding pair. + from triple_lock import datasets + assert data["to"]["dataset"]["sha256"] == datasets.DATASETS[data["to"]["dataset"]["name"]]["sha256"] + assert data["to"]["packages"]["policyengine-uk"] == datasets.CERTIFIED_BUNDLE["model_version"] + assert configuration["pending_release"]["pull_request"] is None @pytest.mark.parametrize("name", ["unregistered@cut", "microcosm_uk_2024_25@", "enhanced_frs_2024_25@ "]) @@ -198,7 +203,7 @@ def test_stub_engine_runs_separate_model_and_data_steps_on_the_same_paired_desig data = run_step("data", "data/pilot/model.json", "data/pilot/data.json") assert len(model_jobs) == len(calls) == 42 assert all(arg["dataset"].endswith("1.58.0") for _, (_, arg) in model_jobs) - assert all(arg["dataset"] == "microcosm_uk_2024_25@a133f37a" for _, (_, arg) in calls) + assert all(arg["dataset"] == "microcosm_uk_2024_25@a6299504" for _, (_, arg) in calls) assert model["provenance"]["dataset"]["certified"] is False assert data["provenance"]["dataset"]["certified"] is True assert model["provenance"]["host_admission"]["minimum_available_gib"] == 40 @@ -411,7 +416,7 @@ def launch(jobs, *, admission, stop, slot_prefix, **kwargs): return [{"synthetic": True}] * len(jobs) monkeypatch.setattr(bridge.jobs, "run_jobs", launch) - batch = [(str(i), ("path", {"dataset": "microcosm_uk_2024_25@a133f37a"})) for i in range(2)] + batch = [(str(i), ("path", {"dataset": "microcosm_uk_2024_25@a6299504"})) for i in range(2)] assert len(bridge.run_batch(batch, 2, tmp_path / "cache")) == 2 # The batch's 80 GiB cold floor is checked before it starts (main); each slot admits one cold job. assert checked and all(row["minimum_gib"] == bridge.COLD_JOB_MINIMUM_GIB for row in checked) diff --git a/tests/test_budget_bundle.py b/tests/test_budget_bundle.py index fae15a36..ee246031 100644 --- a/tests/test_budget_bundle.py +++ b/tests/test_budget_bundle.py @@ -8,30 +8,71 @@ import pytest +import released_pair +from scripts.fill_release_identities import BUNDLE_IDENTITIES, IDENTITIES from triple_lock import certification, datasets, pipeline -from triple_lock.config import PRIMARY_DATASET, REPO, SENSITIVITY_DATASET +from triple_lock.config import PRIMARY_DATASET, SENSITIVITY_DATASET +FALLBACK_DATASET = "microcosm_uk_2024_25@a133f37a" +PENDING = datasets.BINDING_BUNDLE_VERSION is None # before the release-night fill -def test_binding_roles_and_pending_preflight_do_not_read_model_or_data(monkeypatch): - assert PRIMARY_DATASET == "microcosm_uk_2024_25@a133f37a" + +def test_binding_roles_and_the_preflight_read_no_model_or_data(monkeypatch): + assert PRIMARY_DATASET == "microcosm_uk_2024_25@a6299504" + assert datasets.D2011_PAIRS == (PRIMARY_DATASET, FALLBACK_DATASET) assert SENSITIVITY_DATASET == "enhanced_frs_2024_25@1.58.2" monkeypatch.setattr(datasets, "materialize", lambda *a, **kw: pytest.fail("no data")) - monkeypatch.setattr(pipeline, "package_versions", lambda: pytest.fail("no model import")) - with pytest.raises(pipeline.UncertifiedBundle, match="#715.*pending"): + if PENDING: + monkeypatch.setattr(pipeline, "package_versions", lambda: pytest.fail("no model import")) + with pytest.raises(pipeline.UncertifiedBundle, match="d2011.*pending"): + pipeline.certified_bundle_preflight() + else: # filled: an install of any other carrier refuses before provenance + monkeypatch.setattr(pipeline, "package_versions", lambda: {"policyengine": "0.0.0"}) + monkeypatch.setattr(datasets, "provenance", lambda *a: pytest.fail("refused before provenance")) + with pytest.raises(pipeline.UncertifiedBundle, match="d2011"): + pipeline.certified_bundle_preflight() + + +@pytest.mark.parametrize("primary", ["enhanced_frs_2024_25@1.58.2", "microcosm_uk_2024_25@5c6b3f68"]) +def test_the_preflight_admits_only_d2011_s_pairs(monkeypatch, primary): + monkeypatch.setattr(pipeline, "PRIMARY_DATASET", primary) + monkeypatch.setattr(pipeline, "package_versions", lambda: pytest.fail("refused before any import")) + with pytest.raises(pipeline.UncertifiedBundle, match="Microcosm a6299504"): pipeline.certified_bundle_preflight() -@pytest.fixture -def released(tmp_path, monkeypatch): - manifest = json.loads((REPO / "data/certification/d1983-candidate-bundle-manifest.json").read_text()) - version = "99.0.0" # Fictional future release, never a production pin. +def test_the_fallback_preflight_reads_the_fallback_s_own_bundle(monkeypatch): + """After the fallback commit moves the primary, the preflight checks #715's pair, not the 2.151.0 one.""" + monkeypatch.setattr(pipeline, "PRIMARY_DATASET", FALLBACK_DATASET) + for key, value in (("bundle_version", "99.0.0"), ("manifest_sha256", "a" * 64), + ("certification_url", "https://example.invalid/pull/1"), ("certification_commit", "b" * 40)): + monkeypatch.setitem(datasets.BUNDLE_715, key, value) + pins = {"policyengine": "99.0.0", "policyengine-uk": "2.147.2", "policyengine-core": "3.33.3"} + monkeypatch.setattr(pipeline, "package_versions", lambda: dict(pins)) + provenance = {"dataset": FALLBACK_DATASET, "certified": True} + monkeypatch.setattr(datasets, "provenance", lambda name: dict(provenance) if name == FALLBACK_DATASET + else pytest.fail(f"provenance of {name}")) + receipt = pipeline.certified_bundle_preflight(log=lambda _: None) + assert receipt == {"certified_bundle": {"passed": True, "packages": pins, "primary": provenance}} + monkeypatch.setattr(pipeline, "package_versions", lambda: {**pins, "policyengine-uk": "2.151.0"}) + with pytest.raises(pipeline.UncertifiedBundle): + pipeline.certified_bundle_preflight(log=lambda _: None) + + +# d2011's binding pair and its fallback: each must certify once its own release identities are filled. +@pytest.fixture(params=[PRIMARY_DATASET, FALLBACK_DATASET]) +def released(request, tmp_path, monkeypatch): + # #715's a133f37a candidate skeleton, re-pointed at the pair and routed to its local-area cut (the binding's + # invented one while its own is pending), whose manifest SHA-256 is a synthetic manifest's. + manifest, local, _ = released_pair.released(request.param) + version = released_pair.VERSION release = manifest["data_releases"]["uk"] - manifest["policyengine_version"] = version - release["policyengine_version"] = version - release["bundle_id"] = f"uk-{version}" - cert = {**datasets.CERTIFIED_BUNDLE, "bundle_version": version, "bundle_id": f"uk-{version}"} - spec = deepcopy(datasets.DATASETS[PRIMARY_DATASET]) + spec = deepcopy(datasets.DATASETS[request.param]) + cert = {**spec["bundle_certification"], "bundle_version": version, "bundle_id": f"uk-{version}", + "certification_url": "https://example.invalid/pull/9999", "certification_commit": "1" * 40} spec["bundle_certification"] = cert + spec["local_area"] = local + spec["region_datasets"] = datasets.area_routed(local["path"]) path = tmp_path / "manifest.json" distributions = {"policyengine": SimpleNamespace(version=version, locate_file=lambda _: path)} cert["wheels"] = {} @@ -105,6 +146,8 @@ def test_released_bundle_rejects_different_artifact_identity(released, field, va (None, "source_manifest_uri", "hf://other"), (None, "release_manifest_uri", "https://example.com/other"), (None, "region_datasets", {"constituency": {"path_template": "dense.h5"}}), + # National only: the binding pair's constituency and local-authority runs must route to its local-area file. + (None, "region_datasets", {"national": {"path_template": "microcosm_uk_2024_25.h5"}}), ]) def test_structural_pair_checks_even_with_authenticated_manifest(released, location, key, value): spec, versions, manifest, write, _ = released @@ -114,7 +157,158 @@ def test_structural_pair_checks_even_with_authenticated_manifest(released, locat assert not datasets._certified_by_installed_bundle(spec, versions) +@pytest.mark.parametrize("binding,named,pin,certifies", [ + ("certified", "3.33.3", "3.33.3", True), # #715's candidate: the certificate names the core + ("bundle_pin", None, "3.33.3", True), # policyengine.py's default since 6.11.0: it covers the bundle's pin + (None, None, "3.33.3", True), # certificates from before October 2026 follow the pin the same way + ("certified", None, "3.33.3", False), + ("certified", "3.33.0", "3.33.3", False), + ("bundle_pin", "3.33.0", "3.33.3", False), + ("bundle_pin", None, "3.33.0", False), + ("open", None, "3.33.3", False), +]) +def test_the_certified_core_follows_policyengine_pys_core_binding(released, binding, named, pin, certifies): + spec, versions, manifest, write, _ = released + certificate = manifest["data_releases"]["uk"]["certification"] + for key, value in (("core_binding", binding), ("certified_for_core_version", named)): + certificate.pop(key, None) + if value is not None: + certificate[key] = value + manifest["packages"]["policyengine-core"]["version"] = pin + write() + assert datasets._certified_by_installed_bundle(spec, versions) is certifies + + +@pytest.mark.parametrize("key", released_pair.LOCAL_AREA_KEYS) +def test_a_pending_part_of_the_local_area_cut_certifies_nothing(released, key): + spec, versions, _, _, _ = released + assert datasets._certified_by_installed_bundle(spec, versions) + spec["local_area"] = {**spec["local_area"], key: None} + assert not datasets._certified_by_installed_bundle(spec, versions) + + +def test_a_pending_local_area_file_cannot_certify(released): + spec, versions, manifest, write, _ = released + pending = datasets.area_routed(None) + spec["region_datasets"] = deepcopy(pending) + assert not datasets._certified_by_installed_bundle(spec, versions) + manifest["data_releases"]["uk"]["region_datasets"] = deepcopy(pending) + write() + assert not datasets._certified_by_installed_bundle(spec, versions) + + +@pytest.mark.parametrize("key,value", [("revision", "another-cut"), ("sha256", "0" * 64), + ("repo_id", "policyengine/other")]) +def test_another_local_area_cut_of_the_same_file_cannot_certify(released, key, value): + """Every cut publishes microcosm_uk_2024_25_dense.h5: the pinned tag and bytes, not the name, identify it.""" + spec, versions, manifest, write, _ = released + manifest["data_releases"]["uk"]["datasets"]["microcosm_uk_2024_25_dense"][key] = value + write() + assert not datasets._certified_by_installed_bundle(spec, versions) + + +def test_two_datasets_entries_for_the_local_area_file_cannot_certify(released): + """Which entry the routed file means would be ambiguous, even when one of them is the pinned cut.""" + spec, versions, manifest, write, _ = released + entries = manifest["data_releases"]["uk"]["datasets"] + entries["duplicate_dense"] = dict(entries["microcosm_uk_2024_25_dense"]) + write() + assert not datasets._certified_by_installed_bundle(spec, versions) + + +def test_the_basis_of_the_area_routing_is_the_pair_s_own(released): + """The binding pair carries no #699 mismatch record; the fallback needs exactly one.""" + spec, versions, manifest, write, _ = released + release = manifest["data_releases"]["uk"] + if spec["local_area"]["model_basis"] == "built_with": + release["regional_certification"] = released_pair.regional_certification(spec["local_area"], + versions["model_version"]) + else: + del release["regional_certification"] + write() + assert not datasets._certified_by_installed_bundle(spec, versions) + + +@pytest.mark.parametrize("key", ["certification_url", "certification_commit"]) +def test_a_released_certificate_needs_its_certifying_pull_request(released, key): + spec, versions, _, _, _ = released + spec["bundle_certification"][key] = None + assert not datasets._certified_by_installed_bundle(spec, versions) + + +@pytest.mark.parametrize("name,bundle,prefix,model", [ + (PRIMARY_DATASET, "CERTIFIED_BUNDLE", "BINDING", "2.151.0"), + # d2011's fallback is a133f37a with policyengine.py #699's area routing, not #715's national-only candidate. + (FALLBACK_DATASET, "BUNDLE_715", "FALLBACK", "2.147.2"), +]) +def test_each_d2011_pair_routes_areas_to_its_cut_and_is_wholly_pending_or_wholly_filled(name, bundle, prefix, model): + spec = datasets.DATASETS[name] + certification = spec["bundle_certification"] + local = spec["local_area"] + assert certification is getattr(datasets, bundle) and certification["model_version"] == model + assert spec["region_datasets"] == datasets.area_routed(local["path"]) == { + "national": {"path_template": spec["path"]}, + "constituency": {"path_template": local["path"]}, "local_authority": {"path_template": local["path"]}} + filled = [getattr(datasets, f"{prefix}_{key}") is not None for key in IDENTITIES[prefix]] + assert all(filled) or not any(filled) # a half-filled pair is a broken fill + assert (certification["bundle_version"], certification["manifest_sha256"], certification["certification_url"], + certification["certification_commit"]) == tuple(getattr(datasets, f"{prefix}_{key}") + for key in BUNDLE_IDENTITIES) + if not any(filled): + versions = {**{key: certification[key] for key in ("bundle_package", "model_version", "core_version")}, + "bundle_version": "99.0.0"} + assert not datasets._certified_by_installed_bundle(spec, versions) + + +def test_the_local_area_cuts_are_pinned_on_their_bases(): + binding, fallback = datasets.BINDING_LOCAL_AREA, datasets.FALLBACK_LOCAL_AREA + assert datasets.DATASETS[PRIMARY_DATASET]["local_area"] is binding + assert datasets.DATASETS[FALLBACK_DATASET]["local_area"] is fallback + assert (binding["path"], binding["revision"], binding["sha256"], binding["manifest_sha256"]) == ( + datasets.BINDING_LOCAL_AREA_PATH, datasets.BINDING_LOCAL_AREA_REVISION, datasets.BINDING_LOCAL_AREA_SHA256, + datasets.BINDING_LOCAL_AREA_MANIFEST_SHA256) + assert (binding["built_with_model_version"], binding["model_basis"]) == ("2.151.0", "built_with") + # The 19 October dense cut, as its release manifest at the tag records it (61,723 bytes). + assert fallback == { + "path": "microcosm_uk_2024_25_dense.h5", "repo_id": "policyengine/populace-uk-private", "repo_type": "dataset", + "revision": "microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe", + "sha256": "8e10e130d0260e0b52e2e29902f0502ebdff76a14a3e57d2c876bf4cd8db5872", + "release_manifest_uri": "https://huggingface.co/datasets/policyengine/populace-uk-private/resolve/" + "microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe/releases/" + "microcosm-uk-2024-25-dense/release_manifest.json", + "source_manifest_uri": "hf://dataset/policyengine/populace-uk-private@" + "microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe/releases/" + "microcosm-uk-2024-25-dense/release_manifest.json", + "manifest_sha256": "b9dc7cae358b9c965935f83721508594f9fb56a35911ffb0f9216ecb6a799561", + "built_with_model_version": "2.132.6", "model_basis": "accepted_model_mismatch"} + + +def test_the_fallback_keeps_its_own_model_and_release_manifest(): + fallback = datasets.DATASETS[FALLBACK_DATASET] + assert (datasets.BUNDLE_715["model_version"], datasets.BUNDLE_715["core_version"]) == ("2.147.2", "3.33.3") + assert datasets.BUNDLE_715["model_wheel_sha256"] == datasets.BUNDLE_715["wheels"]["policyengine-uk"]["wheel_sha256"] + assert fallback["release_manifest"] == datasets.MICROCOSM_RELEASE_MANIFEST_A133 + assert fallback["build_id"] == fallback["revision"] == datasets.MICROCOSM_REVISION_A133 + + def test_changed_installed_manifest_is_rejected(released): spec, versions, _, _, path = released path.write_text("{}") assert not datasets._certified_by_installed_bundle(spec, versions) + + +@pytest.mark.parametrize("rewrite", [ + lambda raw: json.dumps(json.loads(raw), indent=1).encode(), + lambda raw: json.dumps(json.loads(raw), sort_keys=True).encode(), + lambda raw: raw + b"\n", +], ids=["indented", "sorted", "trailing-newline"]) +def test_an_installed_manifest_with_the_released_content_but_other_bytes_is_rejected(released, rewrite): + """The pinned digest binds the installed manifest's bytes: the same content, written differently, passes every + structural check but must not certify.""" + spec, versions, _, _, path = released + raw = path.read_bytes() + assert datasets._certified_by_installed_bundle(spec, versions) + changed = rewrite(raw) + assert changed != raw and json.loads(changed) == json.loads(raw) + path.write_bytes(changed) + assert not datasets._certified_by_installed_bundle(spec, versions) diff --git a/tests/test_budget_model_projection.py b/tests/test_budget_model_projection.py index 2feb1053..9d8f0cf7 100644 --- a/tests/test_budget_model_projection.py +++ b/tests/test_budget_model_projection.py @@ -14,7 +14,7 @@ LAGGED_CPI = "gov.economic_assumptions.indices.obr.lagged_cpi" SEPTEMBER_CPI = "gov.economic_assumptions.indices.september_cpi_uprating" -# The index gov.benefit_uprating_cpi names on each Budget wheel: #715's 2.147.2 predates #2557; d2011's recommended +# The index gov.benefit_uprating_cpi names on each Budget wheel: #715's 2.147.2 predates #2557; d2011's binding # 2.151.0 has it (and still lacks #2576 and #2340). BENEFIT_INDEX = {"2.147.2": LAGGED_CPI, "2.151.0": SEPTEMBER_CPI} DEPENDENT_PATHS = ["gov.dwp.pension_credit.guarantee_credit.minimum_guarantee.SINGLE", diff --git a/tests/test_certified_preflight.py b/tests/test_certified_preflight.py index 787d3baf..642c27f0 100644 --- a/tests/test_certified_preflight.py +++ b/tests/test_certified_preflight.py @@ -12,15 +12,17 @@ from triple_lock import datasets, pipeline -PINS = {"policyengine": "99.0.0", "policyengine-uk": "2.147.2", "policyengine-core": "3.33.3"} -PRIMARY = "microcosm_uk_2024_25@a133f37a" +PINS = {"policyengine": "99.0.0", "policyengine-uk": "2.151.0", "policyengine-core": "3.33.3"} +PRIMARY = "microcosm_uk_2024_25@a6299504" @pytest.fixture def certified(monkeypatch): - # Fictional released carrier: production pins remain pending. + # A fictional released carrier, whatever the production pins hold. monkeypatch.setitem(datasets.CERTIFIED_BUNDLE, "bundle_version", PINS["policyengine"]) monkeypatch.setitem(datasets.CERTIFIED_BUNDLE, "manifest_sha256", "a" * 64) + monkeypatch.setitem(datasets.CERTIFIED_BUNDLE, "certification_url", "https://example.invalid/pull/1") + monkeypatch.setitem(datasets.CERTIFIED_BUNDLE, "certification_commit", "b" * 40) provenance = {"dataset": PRIMARY, "certified": True, "runtime_dataset_sha256": datasets.DATASETS[PRIMARY]["sha256"], "certification_bundle": dict(datasets.CERTIFIED_BUNDLE), @@ -58,7 +60,7 @@ def test_certified_preflight_refuses_wrong_or_missing_runtime_version(certified, def test_certified_preflight_refuses_other_primary_before_provenance(certified, monkeypatch, primary): monkeypatch.setattr(pipeline, "PRIMARY_DATASET", primary) monkeypatch.setattr(datasets, "provenance", lambda *a: pytest.fail("wrong primary is refused first")) - with pytest.raises(pipeline.UncertifiedBundle, match="Microcosm a133f37a"): + with pytest.raises(pipeline.UncertifiedBundle, match="Microcosm a6299504"): pipeline.certified_bundle_preflight(log=lambda _: None) @@ -66,7 +68,7 @@ def test_certified_preflight_refuses_other_primary_before_provenance(certified, def test_certified_preflight_requires_true_manifest_certification(certified, certification): _, provenance = certified provenance["certified"] = certification - with pytest.raises(pipeline.UncertifiedBundle, match="#715|released manifest"): + with pytest.raises(pipeline.UncertifiedBundle, match="d2011|released manifest"): pipeline.certified_bundle_preflight(log=lambda _: None) @@ -77,10 +79,23 @@ def test_certified_preflight_refuses_mismatched_provenance_primary(certified): pipeline.certified_bundle_preflight(log=lambda _: None) +@pytest.mark.parametrize("field", ["certification_url", "certification_commit"]) +def test_certified_preflight_refuses_a_release_without_its_certifying_pull_request(certified, monkeypatch, field): + monkeypatch.setitem(datasets.CERTIFIED_BUNDLE, field, None) + monkeypatch.setattr(datasets, "provenance", lambda *a: pytest.fail("an unnamed certifier is refused first")) + with pytest.raises(pipeline.UncertifiedBundle, match="certifying pull"): + pipeline.certified_bundle_preflight(log=lambda _: pytest.fail("no pass log")) + + def test_certified_preflight_refuses_tampered_installed_release_manifest(tmp_path, monkeypatch): - """Exercise the real dataset certification helper, without a model import or dataset load.""" + """Exercise the real dataset certification helper, without a model import or dataset load. The carrier's pinned + identities and installed versions match, pending or filled alike, so the preflight reaches provenance: once the + pair is filled the tampered manifest bytes refuse there, and while its local-area cut is pending the cut does.""" manifest = tmp_path / "manifest.json" manifest.write_text("{}") + for key, value in (("bundle_version", PINS["policyengine"]), ("manifest_sha256", "a" * 64), + ("certification_url", "https://example.invalid/pull/1"), ("certification_commit", "b" * 40)): + monkeypatch.setitem(datasets.CERTIFIED_BUNDLE, key, value) versions = {key: datasets.CERTIFIED_BUNDLE[key] for key in ("bundle_package", "bundle_version", "model_version", "core_version")} monkeypatch.setattr(pipeline, "PRIMARY_DATASET", PRIMARY) @@ -90,7 +105,7 @@ def test_certified_preflight_refuses_tampered_installed_release_manifest(tmp_pat monkeypatch.setitem(sys.modules, "policyengine_uk.system", None) monkeypatch.setattr(datasets, "materialize", lambda *a, **kw: pytest.fail("preflight reads no survey bytes")) assert datasets.provenance(PRIMARY)["certified"] is False - with pytest.raises(pipeline.UncertifiedBundle, match="#715|released manifest"): + with pytest.raises(pipeline.UncertifiedBundle, match="installed released manifest does not certify the pinned pair"): pipeline.certified_bundle_preflight(log=lambda _: None) diff --git a/tests/test_datasets.py b/tests/test_datasets.py index e2d8cfc6..c464914e 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -58,16 +58,30 @@ def test_materialize_refuses_a_file_that_does_not_match_its_pin(tmp_path): datasets.materialize(PRIMARY_DATASET, store=tmp_path, download=_fake(b"unused")) -def test_provenance_records_pending_exact_candidate_without_claiming_certification(): +def binding_release_installed(): + """Whether the installed carrier is the binding release datasets.py pins (never while it is pending).""" + return (datasets.BINDING_BUNDLE_VERSION is not None + and datasets.model_versions()["bundle_version"] == datasets.BINDING_BUNDLE_VERSION) + + +def test_provenance_records_the_exact_pair_and_certifies_only_its_installed_release(): + """Pending, or filled but not yet installed: nothing certifies. Filled and installed (CI installs the lock), + the release must certify the pinned pair: the fill's pins and the installed bytes agree.""" pytest.importorskip("policyengine_uk") p = datasets.provenance(PRIMARY_DATASET) - assert p["model_version"] == "2.147.2" == engine.package_versions()["policyengine-uk"] + installed = binding_release_installed() + assert p["model_version"] == "2.151.0" == engine.package_versions()["policyengine-uk"] assert p["core_version"] == "3.33.3" assert p["bundle_version"] == importlib.metadata.version("policyengine") - assert p["certified"] is False and "#715" in p["certification"] - assert "certification_bundle" not in p + assert p["certified"] is installed + if installed: + assert p["certification"] == f"policyengine.py {datasets.BINDING_BUNDLE_VERSION} UK bundle certification" + assert p["certification_bundle"] == datasets.CERTIFIED_BUNDLE + else: + assert "d2011" in p["certification"] and "certification_bundle" not in p assert p["runtime_dataset"] == "microcosm_uk_2024_25" - assert p["runtime_dataset_sha256"] == "64f83900ca3a1242fb017585234844ce89f743d32e41cd2b6558057777f914aa" + assert p["runtime_dataset_sha256"] == "b96a8cd625b645a409c063b1de5158532708a2f0d3c3897a794a8fb0bbd5064e" + assert p["dataset"] == "microcosm_uk_2024_25@a6299504" assert p["release_manifest"] == datasets.MICROCOSM_RELEASE_MANIFEST assert p["data_version"] == "0.1.0" assert p["data_build_id"] == datasets.MICROCOSM_REVISION @@ -97,7 +111,7 @@ def test_certification_rejects_different_artifact_bytes_and_changed_bundle_manif pytest.importorskip("policyengine_uk") spec = datasets.DATASETS[PRIMARY_DATASET] versions = datasets.model_versions() - assert not datasets._certified_by_installed_bundle(spec, versions) + assert datasets._certified_by_installed_bundle(spec, versions) is binding_release_installed() assert not datasets._certified_by_installed_bundle({**spec, "sha256": "0" * 64}, versions) bad_manifest = tmp_path / "manifest.json" bad_manifest.write_text("{}") @@ -108,33 +122,69 @@ def locate_file(self, name): assert not datasets._certified_by_installed_bundle(spec, versions) +def lock_pins(): + from packaging.utils import canonicalize_name + + from triple_lock.config import REPO + + lines = (REPO / "requirements-lock.txt").read_text().splitlines() + return {canonicalize_name(name): version for name, version in + (line.split("==", 1) for line in lines if "==" in line and not line.startswith("#"))} + + def test_the_pins_agree(): - """The project pins the certified carrier; its UK-extra pins and all three installed versions match the lock.""" + """The project pins the carrier (the preparatory one while the release is pending, the release once filled), the + pair's model and core; all three installed versions match the lock.""" import tomllib + from packaging.requirements import Requirement + from triple_lock.config import REPO + carrier = datasets.BINDING_BUNDLE_VERSION or datasets.PREPARATORY_CARRIER + pins = {"policyengine": carrier, "policyengine-uk": "2.151.0", "policyengine-core": "3.33.3"} pyproject = tomllib.loads((REPO / "pyproject.toml").read_text()) dependencies = pyproject["project"]["optional-dependencies"]["uk"] - assert "policyengine==6.10.1" in dependencies - assert "policyengine-uk==2.147.2" in dependencies - assert "policyengine-core==3.33.3" in dependencies + assert {f"{package}=={version}" for package, version in pins.items()} <= set(dependencies) assert not any(d.startswith("policyengine[uk]") for d in dependencies) - lock = (REPO / "requirements-lock.txt").read_text().splitlines() - pins = {"policyengine": "6.10.1", "policyengine-uk": "2.147.2", "policyengine-core": "3.33.3"} - for package, version in pins.items(): - assert f"{package}=={version}" in lock + assert {package: lock_pins().get(package) for package in pins} == pins try: installed = {package: importlib.metadata.version(package) for package in pins} except importlib.metadata.PackageNotFoundError: pytest.skip("certified policyengine UK bundle is not installed") + if datasets.BINDING_BUNDLE_VERSION is not None and installed["policyengine"] != carrier: + pytest.skip(f"the filled carrier {carrier} is not installed yet: re-resolve and install the lock") assert installed == pins + bundled = [Requirement(r) for r in importlib.metadata.requires("policyengine") or []] + uk_extra = {r.name: r.specifier for r in bundled + if r.marker and r.marker.evaluate({"extra": "uk"}) and r.name in pins} + if datasets.BINDING_BUNDLE_VERSION is None: + # The preparatory carrier's extra pins its previous model: installing it would conflict with the pair's. + assert {name: str(specifier) for name, specifier in uk_extra.items()} != { + "policyengine-uk": "==2.151.0", "policyengine-core": "==3.33.3"} + else: + # The release carries the pair, so whatever its extra pins must admit the pair's model and core. + assert all(specifier.contains(pins[name]) for name, specifier in uk_extra.items()) + + +def test_the_lock_satisfies_the_installed_carrier_s_own_requirements(): + """The fill bumps only the carrier pin; a re-resolved lock (docs/REBUILD.md) must still satisfy what that + carrier requires outside its extras.""" from packaging.requirements import Requirement - bundled = [Requirement(r) for r in importlib.metadata.requires("policyengine")] - uk_pins = {r.name: str(r.specifier) for r in bundled - if r.marker and r.marker.evaluate({"extra": "uk"}) and r.name in pins} - assert uk_pins != {"policyengine-uk": "==2.147.2", "policyengine-core": "==3.33.3"} - # The released extra differs: installing it would conflict with the candidate model pin. + from packaging.utils import canonicalize_name + + pins = lock_pins() + try: + installed = importlib.metadata.version("policyengine") + except importlib.metadata.PackageNotFoundError: + pytest.skip("policyengine is not installed") + if installed != pins.get("policyengine"): + pytest.skip(f"installed policyengine {installed} is not the lock's") + unmet = [str(r) for r in map(Requirement, importlib.metadata.requires("policyengine") or []) + if (r.marker is None or r.marker.evaluate({"extra": ""})) + and canonicalize_name(r.name) in pins + and not r.specifier.contains(pins[canonicalize_name(r.name)], prereleases=True)] + assert unmet == [] def test_materialize_hashes_each_file_once_per_process(tmp_path, monkeypatch): diff --git a/tests/test_fill_release_identities.py b/tests/test_fill_release_identities.py new file mode 100644 index 00000000..c77da026 --- /dev/null +++ b/tests/test_fill_release_identities.py @@ -0,0 +1,606 @@ +"""The release-night fill checks every pinned identity and writes only what the release supplies.""" + +import base64 +from copy import deepcopy +import hashlib +import json +from pathlib import Path +import re +import shutil + +import pytest + +from released_pair import INVENTED_CUT, VERSION, regional_certification, released +from scripts import binding_bridge +from scripts import fill_release_identities as fill +from triple_lock import datasets +from triple_lock.config import REPO + +BINDING, FALLBACK = fill.TARGETS["binding"][0], fill.TARGETS["fallback"][0] +MERGED = {"html_url": "https://github.com/PolicyEngine/policyengine.py/pull/9999", "merged_at": "2026-10-26T23:00:00Z", + "merge_commit_sha": "1" * 40, "head": {"sha": "2" * 40}} +OLD = "98.0.0" # The fictional carrier the certifying merge's manifest names before the release's version commit. +ROUTED = "installed check routes to this cut" +REWRITE = "release manifest is the workflow's rewrite of the merge's" +# Real policyengine.py bundle manifests (a public repository), each at src/policyengine/data/bundle/manifest.json: +# two certifying merges and the releases cut from them. +UPSTREAM = Path(__file__).parent / "fixtures/policyengine-bundle-manifests" +UPSTREAM_SHA256 = { + "pr-704-merge-13192b52.json": "461fe51f720501eda986d09e35d779a96269aeff203d4cda17f611ee357b0a73", # 13192b524 + "release-6.11.0.json": "bccf23acde631aed4665757094c04f86e60993fb9d388e7dc958b90c876e5a80", + "pr-555-merge-37d25c42.json": "e7f94bdd88818764e4112e1b6eb74a98d196683ec29ad0a552d38062e701fe8c", # 37d25c423 + "release-6.2.6.json": "8633ad814c9c85e53c7c513f69c4c7b1500a03cd2f6ef133d6e2ea8346b2db54", +} + + +@pytest.fixture +def pending(monkeypatch): + """datasets.py as it stands before any fill, whatever the tree holds: every release identity None.""" + for prefix, names in fill.IDENTITIES.items(): + for key in names: + monkeypatch.setattr(datasets, f"{prefix}_{key}", None) + monkeypatch.setitem(datasets.DATASETS[BINDING], "local_area", + datasets.local_area_cut(None, None, None, None, "2.151.0", "built_with")) + + +def failing(manifest, name, local, regional_raw): + return [label for label, pinned, got in fill.checks(manifest, datasets.DATASETS[name], local, regional_raw) + if pinned != got] + + +@pytest.mark.parametrize("name", [BINDING, FALLBACK]) +def test_a_release_carrying_the_pinned_pair_passes_every_check(pending, name): + manifest, local, regional_raw = released(name) + assert failing(manifest, name, local, regional_raw) == [] + + +@pytest.mark.parametrize("name", [BINDING, FALLBACK]) +def test_a_bundle_pin_certificate_like_6_11_0s_passes(pending, name): + manifest, local, regional_raw = released(name) + certificate = manifest["data_releases"]["uk"]["certification"] + del certificate["certified_for_core_version"] + certificate["core_binding"] = "bundle_pin" + assert failing(manifest, name, local, regional_raw) == [] + + +@pytest.mark.parametrize("name", [BINDING, FALLBACK]) +@pytest.mark.parametrize("where,key,value,labels", [ + ("uk", "policyengine_version", "98.0.0", {"UK release carrier version"}), + ("uk", "bundle_id", "uk-98.0.0", {"bundle id"}), + ("core", "version", "3.33.0", {"core version", "core version certified for"}), + ("certification", "certified_for_core_version", "3.33.0", {"core version certified for"}), + ("model_package", "version", "2.153.4", {"model version"}), + ("certification", "certified_for_model_version", "2.153.4", {"model version certified for"}), + ("model_package", "sha256", "0" * 64, {"model wheel SHA-256"}), + ("uk", "build_id", "other-cut", {"release build id"}), + ("artifact", "build_id", "other-cut", {"artifact build id"}), + ("uk", "release_manifest_uri", "https://example.com/other", {"release manifest URI"}), + ("uk", "source_manifest_uri", "hf://other", {"source manifest URI"}), + ("artifact", "dataset", "enhanced_frs_2024_25", {"artifact dataset"}), + ("artifact", "sha256", "0" * 64, {"artifact SHA-256"}), + ("artifact", "uri", "hf://policyengine/other/microcosm_uk_2024_25.h5@x", {"artifact URI"}), + ("artifact", "data_package", {"name": "microcosm-data", "version": "0.2.0"}, {"artifact data package"}), + # #715's national-only candidate routing, an area type left out or split, or an extra member, is not the pair. + ("uk", "region_datasets", {"national": {"path_template": "microcosm_uk_2024_25.h5"}}, {"region routing"}), + ("regions", "national", {"path_template": "other.h5"}, {"region routing"}), + ("regions", "local_authority", {"path_template": "other_local_area.h5"}, {"region routing"}), + ("regions", "constituency", {"path_template": ""}, {"region routing"}), + ("regions", "constituency", {"path_template": "invented_local_area.h5", "extra": 1}, {"region routing"}), + # The local-area cut as the bundle names it: the installed check refuses each one too. + ("dense", "sha256", "0" * 64, {"local-area datasets entry", ROUTED}), + ("dense", "revision", "other-cut", {"local-area datasets entry", ROUTED}), + ("dense", "repo_id", "policyengine/other", {"local-area datasets entry", ROUTED}), + ("dense", "repo_type", "model", {"local-area datasets entry", ROUTED}), + ("uk", "regional_release_kind", "national", {"regional release kind", ROUTED}), + ("uk", "regional_release_manifest_uri", "https://example.com/other", {"regional release manifest URI", ROUTED}), + ("uk", "regional_source_manifest_uri", "hf://other", {"regional source manifest URI", ROUTED}), + ("uk", "regional_source_manifest_sha256", "0" * 64, {"regional source manifest SHA-256", ROUTED}), +]) +def test_each_pinned_identity_is_checked(pending, name, where, key, value, labels): + manifest, local, regional_raw = released(name) + release = manifest["data_releases"]["uk"] + container = {"uk": release, "core": manifest["packages"]["policyengine-core"], + "certification": release["certification"], "model_package": release["model_package"], + "artifact": release["certified_data_artifact"], "regions": release["region_datasets"], + "dense": release["datasets"]["microcosm_uk_2024_25_dense"]}[where] + container[key] = value + assert labels <= set(failing(manifest, name, local, regional_raw)) + + +@pytest.mark.parametrize("name", [BINDING, FALLBACK]) +@pytest.mark.parametrize("change,label", [ + (lambda m: m["build"].update(cut_tag="other-cut"), "local-area cut tag"), + (lambda m: m["build"]["built_with_model_package"].update(version="2.150.0"), "local-area build model"), + (lambda m: m["build"]["built_with_core_package"].update(version="3.33.0"), "local-area build core"), + (lambda m: m.update(dataset_role="default"), "local-area dataset role"), + (lambda m: m["artifacts"]["microcosm_uk_2024_25_dense"].update(sha256="0" * 64), "local-area artifact"), + (lambda m: m["artifacts"]["microcosm_uk_2024_25_dense"].update(kind="diagnostics"), "local-area artifact"), + (lambda m: m["artifacts"].update(copy=dict(m["artifacts"]["microcosm_uk_2024_25_dense"])), "local-area artifact"), +]) +def test_the_cut_s_own_release_manifest_is_checked(pending, name, change, label): + """Each change also changes the bytes, so the pinned manifest SHA-256 refuses as well.""" + manifest, local, regional_raw = released(name) + regional = json.loads(regional_raw) + change(regional) + assert {label, "local-area release manifest SHA-256"} <= set( + failing(manifest, name, local, json.dumps(regional).encode())) + + +@pytest.mark.parametrize("name", [BINDING, FALLBACK]) +def test_two_datasets_entries_for_the_local_area_file_refuse(pending, name): + """The script and the installed check both refuse, even with both entries naming the pinned cut.""" + manifest, local, regional_raw = released(name) + entries = manifest["data_releases"]["uk"]["datasets"] + entries["duplicate_dense"] = dict(entries["microcosm_uk_2024_25_dense"]) + assert {"local-area datasets entry", ROUTED} <= set(failing(manifest, name, local, regional_raw)) + + +def test_the_binding_pair_refuses_an_accepted_mismatch_block(pending): + """Both halves of d2011's pair are on 2.151.0, so #699's mismatch record is not that pair.""" + manifest, local, regional_raw = released(BINDING) + manifest["data_releases"]["uk"]["regional_certification"] = regional_certification(local, "2.151.0") + assert {"regional certification", ROUTED} <= set(failing(manifest, BINDING, local, regional_raw)) + + +@pytest.mark.parametrize("key,value", [ + ("compatibility_basis", "built_with"), ("certified_for_model_version", "2.151.0"), + ("built_with_model_version", "2.151.0"), ("regional_manifest_uri", "hf://other"), + ("regional_manifest_sha256", "0" * 64), ("area_receipts", "certified"), (None, None), +]) +def test_the_fallback_needs_699_s_exact_mismatch_record(pending, key, value): + manifest, local, regional_raw = released(FALLBACK) + release = manifest["data_releases"]["uk"] + if key is None: + del release["regional_certification"] + else: + release["regional_certification"][key] = value + assert {"regional certification", ROUTED} <= set(failing(manifest, FALLBACK, local, regional_raw)) + + +def test_the_fallback_needs_the_regional_manifest_digest(pending): + manifest, local, regional_raw = released(FALLBACK) + del manifest["data_releases"]["uk"]["regional_source_manifest_sha256"] + assert {"regional source manifest SHA-256", ROUTED} <= set(failing(manifest, FALLBACK, local, regional_raw)) + + +@pytest.mark.parametrize("digest", [True, False]) +def test_the_binding_digest_is_compared_where_the_release_records_it(pending, digest): + """A 6.11.0-shaped release has no regional_source_manifest_sha256; #699's schema adds it.""" + manifest, local, regional_raw = released(BINDING) + if digest: + manifest["data_releases"]["uk"]["regional_source_manifest_sha256"] = local["manifest_sha256"] + assert failing(manifest, BINDING, local, regional_raw) == [] + if digest: + manifest["data_releases"]["uk"]["regional_source_manifest_sha256"] = "0" * 64 + assert {"regional source manifest SHA-256", ROUTED} <= set(failing(manifest, BINDING, local, regional_raw)) + + +def test_identities_come_from_the_exact_bytes_the_merge_and_the_cut(pending): + manifest, local, regional_raw = released(BINDING) + raw = json.dumps(manifest).encode() + assert fill.identities(raw, manifest, MERGED, "BINDING", regional_raw) == { + "BUNDLE_VERSION": VERSION, "BUNDLE_MANIFEST_SHA256": hashlib.sha256(raw).hexdigest(), + "CERTIFICATION_URL": MERGED["html_url"], "CERTIFICATION_COMMIT": MERGED["merge_commit_sha"], + "LOCAL_AREA_PATH": local["path"], "LOCAL_AREA_REVISION": local["revision"], + "LOCAL_AREA_SHA256": local["sha256"], "LOCAL_AREA_MANIFEST_SHA256": hashlib.sha256(regional_raw).hexdigest()} + assert set(fill.identities(raw, manifest, MERGED, "FALLBACK", regional_raw)) == set(fill.BUNDLE_IDENTITIES) + candidate = {**MERGED, "merged_at": None} + assert fill.identities(raw, manifest, candidate, "BINDING", regional_raw)["CERTIFICATION_COMMIT"] is None + + +def written(manifest): + """A manifest's bytes as the release workflow writes them (json.dumps, indent 2, sorted keys, final newline).""" + return (json.dumps(manifest, indent=2, sort_keys=True) + "\n").encode() + + +def as_merged(manifest): + """The manifest as the certifying merge left it, before the release workflow's version commit: each field that + commit rewrites names the carrier OLD. Written out field by field, not derived from fill.as_released.""" + merged = deepcopy(manifest) + merged["bundle_version"] = merged["policyengine_version"] = OLD + merged["packages"]["policyengine"].update(version=OLD, install_requirement=f"policyengine=={OLD}") + merged["citation"].update(title=f"PolicyEngine bundle {OLD}", version=OLD) + for country, data_release in merged["data_releases"].items(): + data_release.update(policyengine_version=OLD, bundle_id=f"{country}-{OLD}") + return merged + + +def upstream(name): + raw = (UPSTREAM / name).read_bytes() + assert hashlib.sha256(raw).hexdigest() == UPSTREAM_SHA256[name] + return raw + + +@pytest.mark.parametrize("merge,release,version", [("pr-704-merge-13192b52.json", "release-6.11.0.json", "6.11.0"), + ("pr-555-merge-37d25c42.json", "release-6.2.6.json", "6.2.6")]) +def test_the_replayed_workflow_writes_each_real_release_s_exact_bytes(merge, release, version): + """Differential: the script's replay against what policyengine.py's own release workflow wrote.""" + assert fill.manifest_text(fill.as_released(json.loads(upstream(merge)), version)) == upstream(release) + + +def test_a_real_certifier_whose_manifest_is_not_the_release_s_is_refused(): + """#555 (6.2.6's certifier) is an ancestor of 6.11.0, but 6.11.0's manifest is not its rewrite.""" + merged = json.loads(upstream("pr-555-merge-37d25c42.json")) + assert fill.manifest_text(fill.as_released(merged, "6.11.0")) != upstream("release-6.11.0.json") + + +def test_a_real_release_with_an_old_version_source_uri_is_refused(): + """The review's input: a source URI naming the old carrier in the merge and the new one in the release passed + the earlier string replacement. The workflow never rewrites a source URI, so it must be the merge's exactly.""" + merged = json.loads(upstream("pr-704-merge-13192b52.json")) + release = json.loads(upstream("release-6.11.0.json")) + assert fill.manifest_text(fill.as_released(merged, "6.11.0")) == written(release) + merged["data_releases"]["us"]["source_manifest_uri"] = "hf://dataset/example/data@6.10.2/release_manifest.json" + release["data_releases"]["us"]["source_manifest_uri"] = "hf://dataset/example/data@6.11.0/release_manifest.json" + assert fill.manifest_text(fill.as_released(merged, "6.11.0")) != written(release) + + +NON_ASCII = "caf\u00e9 \u00a3 \u4e2d \U0001f600" + + +def test_non_ascii_text_is_written_escaped_as_the_workflow_writes_it(): + """json.dumps escapes non-ASCII by default, upstream and here, whether the merge's bytes were escaped or UTF-8.""" + release = json.loads(upstream("release-6.11.0.json")) + release["data_releases"]["uk"]["note"] = NON_ASCII + for ensure_ascii in (True, False): + merged = json.loads(upstream("pr-704-merge-13192b52.json")) + merged["data_releases"]["uk"]["note"] = NON_ASCII + merged = json.loads(json.dumps(merged, ensure_ascii=ensure_ascii).encode("utf-8")) + assert fill.manifest_text(fill.as_released(merged, "6.11.0")) == written(release) + assert b"\\u00e9" in written(release) and NON_ASCII.encode("utf-8") not in written(release) + + +@pytest.mark.parametrize("entry", [None, "retired", ["uk"]]) +def test_a_non_dict_data_release_is_left_as_the_workflow_leaves_it(pending, entry): + """sync_bundle_versions rewrites only the dict entries of data_releases.""" + manifest = released(BINDING)[0] + manifest["data_releases"]["xx"] = entry + merged = as_merged({**manifest, "data_releases": {k: v for k, v in manifest["data_releases"].items() if k != "xx"}}) + merged["data_releases"]["xx"] = deepcopy(entry) + assert fill.manifest_text(fill.as_released(merged, VERSION)) == written(manifest) + + +def test_the_synthetic_release_is_the_workflow_s_rewrite_of_its_merge(pending): + for name in (BINDING, FALLBACK): + manifest = released(name)[0] + assert manifest["policyengine_version"] == VERSION and as_merged(manifest)["policyengine_version"] == OLD + assert fill.manifest_text(fill.as_released(as_merged(manifest), VERSION)) == written(manifest) + assert fill.manifest_text(fill.as_released(manifest, VERSION)) == written(manifest) + # The workflow derives the whole citation and drops ``source``, whatever the merge wrote there. + merged = as_merged(manifest) + merged["citation"]["doi"], merged["source"] = "10.5281/zenodo.1", {"pull_request": 9999} + assert fill.manifest_text(fill.as_released(merged, VERSION)) == written(manifest) + + +@pytest.mark.parametrize("in_merge,in_release", [ + (lambda m: m["data_releases"]["uk"]["certified_data_artifact"].update(sha256="0" * 64), None), + (lambda m: m["data_releases"]["uk"].update(extra=None), None), + (lambda m: m["data_releases"]["uk"]["region_datasets"].pop("constituency"), None), + (lambda m: m["data_releases"]["uk"]["certification"].update(core_binding=1), None), + (lambda m: m.update(schema_version=str(m["schema_version"])), None), + (lambda m: m["packages"]["policyengine-uk"].update(version=OLD), None), + (lambda m: m["packages"]["policyengine"].update(markers="python_version >= '3.13'"), None), + # Strings naming the carrier outside the rewritten fields: old in the merge, new in the release. + (lambda m: m["data_releases"]["uk"].update(source_manifest_uri=f"hf://dataset/example@{OLD}/manifest.json"), + lambda m: m["data_releases"]["uk"].update(source_manifest_uri=f"hf://dataset/example@{VERSION}/manifest.json")), + (lambda m: m["data_releases"]["us"].update(release_manifest_uri=f"https://example.com/{OLD}/manifest.json"), + lambda m: m["data_releases"]["us"].update(release_manifest_uri=f"https://example.com/{VERSION}/manifest.json")), + # Values Python compares equal whose bytes differ: the merge wrote 1, the release true. + (lambda m: m["data_releases"]["uk"].update(flag=1), lambda m: m["data_releases"]["uk"].update(flag=True)), +]) +def test_the_release_may_differ_from_the_merge_only_in_the_rewritten_fields(pending, in_merge, in_release): + manifest = released(BINDING)[0] + merged = as_merged(manifest) + in_merge(merged) + if in_release: + in_release(manifest) + assert fill.manifest_text(fill.as_released(merged, VERSION)) != written(manifest) + + +def test_the_workflow_derives_every_package_s_install_requirement(pending): + """normalized_manifest rewrites each package's requirement from its name, version and markers.""" + manifest = released(BINDING)[0] + markers = "python_version >= '3.13'" + manifest["packages"]["policyengine-us"].update(markers=markers, + install_requirement=f"policyengine-us==2.2.1; {markers}") + merged = as_merged(manifest) + merged["packages"]["policyengine-us"]["install_requirement"] = "policyengine-us" + merged["packages"]["policyengine-uk"]["install_requirement"] = "policyengine-uk>=2" + assert fill.manifest_text(fill.as_released(merged, VERSION)) == written(manifest) + + +@pytest.mark.parametrize("field", ["bundle_version", "policyengine_version", "packages"]) +def test_a_merge_missing_a_field_the_version_commit_needs_is_no_release(pending, field): + """sync_bundle_versions exits 1 without bundle_version, policyengine_version or packages.""" + merged = as_merged(released(BINDING)[0]) + del merged[field] + assert fill.as_released(merged, VERSION) is None + + +@pytest.mark.parametrize("merged", [None, [], {"policyengine_version": "6.2.6", "data_releases": {}}, + {"bundle_version": OLD, "policyengine_version": OLD, "packages": {}}, + {"bundle_version": OLD, "policyengine_version": OLD, + "packages": {"policyengine": {"version": OLD}}}]) +def test_a_merge_manifest_the_workflow_cannot_rewrite_is_no_release(merged): + assert fill.as_released(merged, VERSION) is None + + +def hub(manifest, merged=None, status="ahead", pull_request=MERGED, tag_bytes=written): + """gh api, answering only the reads a --release run should make: the manifest at the tag (as ``tag_bytes`` + writes it) and at the merge commit, the pull request, and the merge commit's comparison with the tag. Any other + read fails.""" + merged = as_merged(manifest) if merged is None else merged + encode = lambda raw: {"content": base64.b64encode(raw).decode()} + commit = pull_request["merge_commit_sha"] + answers = {f"repos/{fill.REPOSITORY}/contents/{fill.MANIFEST}?ref={VERSION}": encode(tag_bytes(manifest)), + f"repos/{fill.REPOSITORY}/contents/{fill.MANIFEST}?ref={commit}": encode(written(merged)), + f"repos/{fill.REPOSITORY}/pulls/9999": pull_request, + f"repos/{fill.REPOSITORY}/compare/{commit}...{VERSION}": {"status": status}} + return answers.__getitem__ + + +@pytest.mark.parametrize("pull_request,status,merged,refusal", [ + (MERGED, "ahead", None, None), + (MERGED, "identical", None, None), + ({**MERGED, "merged_at": None}, "ahead", None, "certifying pull request merged"), + (MERGED, "diverged", None, "merge commit is in the release"), + (MERGED, "behind", None, "merge commit is in the release"), + # A certifier that is an ancestor of the tag but whose manifest is not the release's (like 6.2.6's #555). + (MERGED, "ahead", {"policyengine_version": "6.2.6", "data_releases": {}}, REWRITE), +]) +def test_the_certifying_pull_request_merged_into_the_tag_with_its_manifest(pending, monkeypatch, pull_request, status, + merged, refusal): + manifest = released(BINDING)[0] + monkeypatch.setattr(fill, "github", hub(manifest, merged, status, pull_request)) + failures = [label for label, pinned, got in fill.certifying_checks(pull_request, VERSION, written(manifest)) + if pinned != got] + assert failures == ([refusal] if refusal else []) + + +@pytest.mark.parametrize("merge_value,release_value", [ + (f"hf://dataset/example@{OLD}/manifest.json", f"hf://dataset/example@{VERSION}/manifest.json"), + (1, True), # equal in Python, not in the bytes +]) +def test_the_certifying_check_compares_the_tag_s_bytes(pending, monkeypatch, merge_value, release_value): + manifest = released(BINDING)[0] + merged = as_merged(manifest) + merged["data_releases"]["uk"]["source_manifest_uri"] = merge_value + manifest["data_releases"]["uk"]["source_manifest_uri"] = release_value + monkeypatch.setattr(fill, "github", hub(manifest, merged)) + failures = [label for label, pinned, got in fill.certifying_checks(MERGED, VERSION, written(manifest)) + if pinned != got] + assert failures == [REWRITE] + + +# Tags equal to the rewrite in content but not in bytes. +REFORMATTED = [lambda m: json.dumps(m).encode(), lambda m: written(m)[:-1], + lambda m: json.dumps(m, indent=2, sort_keys=False).encode() + b"\n", + lambda m: (json.dumps(m, indent=2, sort_keys=True, ensure_ascii=False) + "\n").encode()] + + +@pytest.mark.parametrize("tag_bytes", REFORMATTED) +def test_the_certifying_check_refuses_a_tag_reformatted_from_the_rewrite(pending, monkeypatch, tag_bytes): + manifest = released(BINDING)[0] + manifest["data_releases"]["uk"]["note"] = NON_ASCII + merged = as_merged(manifest) + assert json.loads(tag_bytes(manifest)) == fill.as_released(merged, VERSION) + assert tag_bytes(manifest) != written(manifest) + monkeypatch.setattr(fill, "github", hub(manifest, merged)) + failures = [label for label, pinned, got in fill.certifying_checks(MERGED, VERSION, tag_bytes(manifest)) + if pinned != got] + assert failures == [REWRITE] + + +def reset(root): + """Copy the files the fill writes, with every release identity and the carrier back at their pending values.""" + for relative in ("src/triple_lock/datasets.py", "pyproject.toml", "requirements-lock.txt", + "data/binding-bridge.pending.json"): + (root / relative).parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(REPO / relative, root / relative) + path = root / "src/triple_lock/datasets.py" + text = path.read_text() + for prefix, names in fill.IDENTITIES.items(): + for key in names: + text, count = re.subn(rf"^{prefix}_{key} = .*$", f"{prefix}_{key} = None", text, flags=re.M) + assert count == 1 + path.write_text(text) + for path in (root / "pyproject.toml", root / "requirements-lock.txt"): + text, count = re.subn(r'^(\s*"?policyengine==)[0-9][^",\s]*', rf"\g<1>{datasets.PREPARATORY_CARRIER}", + path.read_text(), flags=re.M) + assert count == 1 + path.write_text(text) + return root + + +@pytest.fixture +def tree(tmp_path): + """A pending copy of the tree, so the real one is never touched and its fill state does not matter.""" + return reset(tmp_path / "tree") + + +def values(prefix="BINDING"): + manifest, _, regional_raw = released(BINDING) + return fill.identities(json.dumps(manifest).encode(), manifest, MERGED, prefix, regional_raw) + + +def assignments(text, prefix): + return {name: line.split(" = ", 1)[1] for line in text.splitlines() + for name in fill.IDENTITIES[prefix] if line.startswith(f"{prefix}_{name} = ")} + + +def files(root): + return {path.relative_to(root).as_posix(): path.read_bytes() for path in root.rglob("*") if path.is_file()} + + +def test_the_binding_fill_writes_every_pin_and_a_ready_bridge(pending, tree): + filled = values() + fill.fill(tree, "BINDING", filled, MERGED["head"]["sha"]) + text = (tree / "src/triple_lock/datasets.py").read_text() + assert assignments(text, "BINDING") == {name: json.dumps(filled[name]) for name in fill.IDENTITIES["BINDING"]} + assert set(assignments(text, "FALLBACK").values()) == {"None"} + assert f'"policyengine=={VERSION}",' in (tree / "pyproject.toml").read_text() + assert f"\npolicyengine=={VERSION}\n" in (tree / "requirements-lock.txt").read_text() + pending_bridge = json.loads((REPO / "data/binding-bridge.pending.json").read_text()) + assert json.loads((tree / "data/binding-bridge.pending.json").read_text()) == pending_bridge + ready = json.loads((tree / "data/binding-bridge.json").read_text()) + binding_bridge.validate_configuration(ready) + assert ready["pending_release"] == {**pending_bridge["pending_release"], "pull_request": MERGED["html_url"], + "reviewed_head": MERGED["head"]["sha"], "policyengine_version": VERSION, + "bundle_manifest_sha256": filled["BUNDLE_MANIFEST_SHA256"]} + # Only the three carrier pins and the status change: model, core and dataset pins stay exactly as reviewed. + expected = deepcopy(pending_bridge) + expected["status"] = "ready" + expected["pending_release"] = ready["pending_release"] + for step, endpoint in (("model", "to"), ("data", "from"), ("data", "to")): + expected["steps"][step][endpoint]["packages"]["policyengine"] = VERSION + assert ready == expected + + +def test_the_fallback_fill_writes_only_its_own_identities(pending, tree): + before = files(tree) + fill.fill(tree, "FALLBACK", values("FALLBACK"), MERGED["head"]["sha"]) + text = (tree / "src/triple_lock/datasets.py").read_text() + assert "None" not in set(assignments(text, "FALLBACK").values()) + assert set(assignments(text, "BINDING").values()) == {"None"} + assert {path for path, raw in files(tree).items() if before.get(path) != raw} == {"src/triple_lock/datasets.py"} + + +def test_a_value_is_written_as_itself_never_as_a_template(pending, tree): + """A backslash in a value is not a regex back-reference.""" + url = r"https://github.com/PolicyEngine/policyengine.py/pull/9999\1\g<0>" + fill.fill(tree, "BINDING", {**values(), "CERTIFICATION_URL": url}, MERGED["head"]["sha"]) + assert f"BINDING_CERTIFICATION_URL = {json.dumps(url)}\n" in (tree / "src/triple_lock/datasets.py").read_text() + + +@pytest.mark.parametrize("missing", ["CERTIFICATION_COMMIT", "LOCAL_AREA_REVISION", "LOCAL_AREA_MANIFEST_SHA256"]) +def test_a_value_the_release_does_not_supply_writes_nothing(pending, tree, missing): + before = files(tree) + with pytest.raises(ValueError, match=missing): + fill.fill(tree, "BINDING", {**values(), missing: None}, MERGED["head"]["sha"]) + assert files(tree) == before + + +@pytest.mark.parametrize("break_tree", [ + # A pending endpoint that already names a carrier, found after datasets.py and both pins were built. + lambda root: (root / "data/binding-bridge.pending.json").write_text( + (root / "data/binding-bridge.pending.json").read_text().replace('"policyengine": null', '"policyengine": "1"', 1)), + # No exact carrier pin to replace, found after datasets.py was built. + lambda root: (root / "pyproject.toml").write_text( + (root / "pyproject.toml").read_text().replace("policyengine==", "policyengine>=")), +]) +def test_a_refusal_found_late_still_writes_nothing(pending, tree, break_tree): + break_tree(tree) + before = files(tree) + with pytest.raises(ValueError, match="nothing is written"): + fill.fill(tree, "BINDING", values(), MERGED["head"]["sha"]) + assert files(tree) == before + + +def run_release(monkeypatch, tree, manifest, regional_raw, *arguments, merged=None, tag_bytes=written): + """main() in --release mode against the fake GitHub and Hub; the Hub read must be at the cut's own tag.""" + monkeypatch.setattr(fill, "github", hub(manifest, merged, tag_bytes=tag_bytes)) + monkeypatch.setattr(fill, "ROOT", tree) + cut_tag = json.loads(regional_raw)["build"]["cut_tag"] + monkeypatch.setattr(fill, "hub_file", lambda local: regional_raw if local["revision"] == cut_tag + else pytest.fail(f"read the cut's manifest at {local['revision']}")) + return fill.main(["--pr", "9999", "--release", VERSION, *arguments]) + + +def test_the_release_path_reads_the_tag_and_applies_the_binding_pair(pending, tree, monkeypatch): + manifest, local, regional_raw = released(BINDING) + assert run_release(monkeypatch, tree, manifest, regional_raw, "--apply") == 0 + filled = assignments((tree / "src/triple_lock/datasets.py").read_text(), "BINDING") + assert filled["LOCAL_AREA_REVISION"] == json.dumps(local["revision"]) + assert filled["LOCAL_AREA_MANIFEST_SHA256"] == json.dumps(local["manifest_sha256"]) + assert filled["CERTIFICATION_COMMIT"] == json.dumps(MERGED["merge_commit_sha"]) + assert (tree / "data/binding-bridge.json").exists() + + +def test_the_release_path_applies_the_pinned_fallback_cut(pending, tree, monkeypatch): + manifest, local, regional_raw = released(FALLBACK) + monkeypatch.setitem(datasets.DATASETS[FALLBACK], "local_area", local) + assert run_release(monkeypatch, tree, manifest, regional_raw, "--target", "fallback", "--apply") == 0 + assert "None" not in assignments((tree / "src/triple_lock/datasets.py").read_text(), "FALLBACK").values() + assert not (tree / "data/binding-bridge.json").exists() + + +def test_the_release_path_refuses_a_tag_naming_another_carrier(pending, monkeypatch): + manifest = released(BINDING, version="98.5.0")[0] + monkeypatch.setattr(fill, "github", lambda path: {"content": base64.b64encode(json.dumps(manifest).encode())}) + with pytest.raises(SystemExit, match="names policyengine 98.5.0"): + fill.main(["--pr", "9999", "--release", VERSION]) + + +@pytest.mark.parametrize("change", [ + lambda manifest: manifest["data_releases"]["uk"]["certified_data_artifact"].update(sha256="0" * 64), + lambda manifest: manifest["data_releases"]["uk"].update(regional_certification={"compatibility_basis": "x"}), +]) +def test_main_refuses_a_differing_release_before_writing(pending, tree, monkeypatch, capsys, change): + manifest, _, regional_raw = released(BINDING) + change(manifest) + before = files(tree) + assert run_release(monkeypatch, tree, manifest, regional_raw, "--apply") == 1 + assert files(tree) == before + assert "refused" in capsys.readouterr().err + + +def test_main_refuses_a_release_that_is_not_the_certifying_merge(pending, tree, monkeypatch, capsys): + manifest, _, regional_raw = released(BINDING) + merged = as_merged(manifest) + merged["data_releases"]["uk"]["certified_data_artifact"]["sha256"] = "0" * 64 + before = files(tree) + assert run_release(monkeypatch, tree, manifest, regional_raw, "--apply", merged=merged) == 1 + assert files(tree) == before + assert f"FAIL {REWRITE}" in capsys.readouterr().out + + +@pytest.mark.parametrize("tag_bytes", REFORMATTED) +def test_main_compares_the_tag_s_own_bytes(pending, tree, monkeypatch, capsys, tag_bytes): + """The tag's bytes, not a re-serialization of them, must be the workflow's.""" + manifest, _, regional_raw = released(BINDING) + manifest["data_releases"]["uk"]["note"] = NON_ASCII + before = files(tree) + assert run_release(monkeypatch, tree, manifest, regional_raw, "--apply", tag_bytes=tag_bytes) == 1 + assert files(tree) == before + assert f"FAIL {REWRITE}" in capsys.readouterr().out + assert run_release(monkeypatch, tree, manifest, regional_raw) == 0 + + +def test_the_binding_pair_refuses_a_local_area_cut_built_on_another_model(pending, tree, monkeypatch, capsys): + """The review's failing input: a6299504 released with a cut shaped like the 19 October one (built on 2.132.6).""" + other = datasets.local_area_cut(INVENTED_CUT["path"], "dense-cut-built-on-2.132.6", "e" * 64, None, "2.132.6", + "built_with") + manifest, _, regional_raw = released(BINDING, local=other) + assert run_release(monkeypatch, tree, manifest, regional_raw) == 1 + assert "FAIL local-area build model" in capsys.readouterr().out + manifest["data_releases"]["uk"]["regional_certification"] = regional_certification(other, "2.151.0") + assert run_release(monkeypatch, tree, manifest, regional_raw) == 1 + assert "FAIL regional certification" in capsys.readouterr().out + + +def test_apply_needs_the_release_path(pending, tmp_path): + path = tmp_path / "manifest.json" + path.write_text(json.dumps(released(BINDING)[0])) + with pytest.raises(SystemExit) as refusal: + fill.main(["--pr", "9999", "--manifest", str(path), "--apply"]) + assert refusal.value.code == 2 + + +def test_a_rerun_checks_the_identities_already_filled(pending, tmp_path, monkeypatch, capsys): + manifest, local, regional_raw = released(BINDING) + path = tmp_path / "manifest.json" + path.write_text(json.dumps(manifest)) + regional = tmp_path / "regional.json" + regional.write_bytes(regional_raw) + filled = fill.identities(path.read_bytes(), manifest, MERGED, "BINDING", regional_raw) + monkeypatch.setattr(fill, "github", {f"repos/{fill.REPOSITORY}/pulls/9999": MERGED}.__getitem__) + for key, value in filled.items(): + monkeypatch.setattr(datasets, f"BINDING_{key}", value) + monkeypatch.setitem(datasets.DATASETS[BINDING], "local_area", local) + arguments = ["--pr", "9999", "--manifest", str(path), "--regional-manifest", str(regional)] + assert fill.main(arguments) == 0 + assert capsys.readouterr().out.count("ok filled BINDING_") == len(fill.IDENTITIES["BINDING"]) + # Installed bytes that differ from the pinned digest refuse. + path.write_text(json.dumps(manifest, indent=1)) + assert fill.main(arguments) == 1 + assert "FAIL filled BINDING_BUNDLE_MANIFEST_SHA256" in capsys.readouterr().out diff --git a/tests/test_microcosm_primary.py b/tests/test_microcosm_primary.py index deccd86b..215a723e 100644 --- a/tests/test_microcosm_primary.py +++ b/tests/test_microcosm_primary.py @@ -9,7 +9,7 @@ def test_binding_roles_and_current_wording_preserve_frozen_pipeline_constants(): - assert config.PRIMARY_DATASET == "microcosm_uk_2024_25@a133f37a" + assert config.PRIMARY_DATASET == "microcosm_uk_2024_25@a6299504" assert config.SENSITIVITY_DATASET == "enhanced_frs_2024_25@1.58.2" assert datasets.resolve() == config.PRIMARY_DATASET assert datasets.ageing_anchor()["year"] == 2025 @@ -20,7 +20,8 @@ def test_binding_roles_and_current_wording_preserve_frozen_pipeline_constants(): assert "Microcosm is an uncertified sensitivity" in " ".join(pipeline.METHOD_LIMITATIONS) current = " ".join(pipeline.method_limitations()) assert "Microcosm is an uncertified sensitivity" not in current - for wanted in ("#715", "2.147.2", "3.33.3", "1.58.2", "#2557", "#2576", "d1753", "Vahid", "step (b)"): + for wanted in ("d2011", "a6299504", "2.151.0", "3.33.3", "1.58.2", "#2557", "#2576", "d1753", "Vahid", + "step (b)"): assert wanted in current # Preserve exactly the historical imports/constants and redactor that C2 protects. import cold_source_correspondence as cold